##Introduction This notebook leverages 20th Edition of Geolytix Open
Supermarket Retail Points data set to perform some analytics on
geospatial data.
The perceived task at hand assumes a large supermarket chain in the
UK (Tesco) is interesting in acquiring a competitor chain, following
which the challenge is to identify reasonable geographic regions into
which to segment he stores, and most appropriate locations to position
warehouse depots for stocking the stores.
The approach taken makes use of clustering techniques, as well as
third part applications (run on Docker) to arrive at a solution.
The intention is to provide an example of how to work with geospatial
data, in a way that could be applicable to a range of different
industries - from logistics to marketing - and sectors - from retail to
healthcare.
Main Run
# load starting data
all_stores <- read_xls('data/GEOLYTIX - UK RetailPoints/uk_glx_open_retail_points_v24_202206.xls')
# plot store count by retailer
retailer_count <- all_stores %>%
group_by(retailer) %>%
tally()
ggplot(retailer_count, aes(x = reorder(retailer, -n), y = n)) +
geom_bar(stat = "identity") + theme_minimal() +
theme(axis.text.x = element_text(angle = 90, hjust = 1, vjust = 0.3))

# plot store count by cluster as %
retailer_count <- retailer_count %>%
mutate(percentage = (n / sum(n))*100)
ggplot(retailer_count, aes(x = reorder(retailer, -percentage), y = percentage)) +
geom_bar(stat = "identity") + theme_minimal() +
theme(axis.text.x = element_text(angle = 90, hjust = 1, vjust = 0.3))

# create list of largest stores
majors <- c("Aldi", "Asda", "Lidl", "Marks and Spencer", "Morrisons",
"Sainsburys", "Tesco", "The Co-operative Group", "Waitrose")
# compute % of stores captured by major chains
print(retailer_count %>% filter(retailer %in% majors) %>% pull(percentage) %>% sum)
[1] 66.97561
# 66.975 %
# the major chains capture 2/3rds of the universe
# plot location of major chains
major_retailers <- all_stores %>% filter(`retailer` %in% majors) %>%
select(id, retailer, postcode, lng = long_wgs, lat = lat_wgs, size_band)
pal <- colorFactor(
palette = c(
"cyan", "green", "purple", "black", "yellow", "orange", "blue", "turquoise", "red"
),
domain = major_retailers$retailer)
p <- leaflet(major_retailers) %>% addTiles() %>%
addCircles(~lng, ~lat,color = ~pal(retailer)) %>%
overlayTitle("Major Retailer Store Locations") %>%
addLegend(position = "bottomright", values = ~retailer, pal = pal); p
Purchase of Competitor
Assuming we are Tesco (as the largest supermarket) and are looking to
acquire one of the other chains, we could need to set a series of
conditions under which we would acquire a competitor. In this example,
let’s imagine these are simply:
Acquire based on which chain offers the “best coverage”- where
there is minimum overlap with existing Tesco locations. i.e. find the
competitor where least % of stores within a certain radius of Tesco
stores
Of the stores with minimal overlap in coverage, which have a
profile of store locations that best matches Tesco’s business
strategy
# we will set a cut-off of 500m each other - based on Haversine distance
major_retailers <- major_retailers %>%
rowwise %>%
mutate(coordinates = list(c(lng, lat))) %>%
ungroup
# isolate the tesco locations
tesco_loc <- major_retailers %>%
filter(retailer == "Tesco")
# obtain list of all other target brands
targets <- majors[majors != 'Tesco']
# iterate and compute % of each brand within 500m
for (brand in targets){
target_loc <- major_retailers %>%
filter(retailer == brand)
close_stores <- calcCloseStore(tesco_loc, target_loc)
print(brand)
print((close_stores/nrow(target_loc))*100)
}
[1] "Aldi"
[1] 26.34298
[1] "Asda"
[1] 14.28571
[1] "Lidl"
[1] 29.41788
[1] "Marks and Spencer"
[1] 33.98876
[1] "Morrisons"
[1] 12.31964
[1] "Sainsburys"
[1] 33.30966
[1] "The Co-operative Group"
[1] 15.45048
[1] "Waitrose"
[1] 35.18006
# ------results
# "Aldi"
# 26.34298
# "Asda"
# 14.28571
# "Lidl"
# 29.41788
# "Marks and Spencer"
# 33.98876
# "Morrisons"
# 12.31964
# "Sainsburys"
# 33.30966
# "The Co-operative Group"
# 15.45048
# "Waitrose"
# 35.18006
From the above results we see that Morrisons and Asda have the lowest
%. We can now explore the profile of their respective store portfolios
(to see the number of stores these brands offer by size)
print(retailer_count %>% filter(retailer %in% c('Asda', 'Morrisons', 'The Co-operative Group')))
# Asda 637
# Morrisons 901
# Coop 2686
# as well as the array of store sizes - and how this compares to Tesco
store_size_counts <- all_stores %>%
filter(retailer %in% c('Tesco', 'Asda', 'Morrisons', 'The Co-operative Group')) %>%
group_by(retailer, size_band) %>%
tally() %>%
group_by(retailer) %>%
mutate(percentage_stores = (n / sum(n))*100)
ggplot(store_size_counts, aes(x = size_band, y = percentage_stores, fill=retailer)) +
geom_col(position = "dodge") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 90, hjust = 1, vjust = 0.3))

From glancing at the charts, it is fairly obvious on first inspection
that’s Morrisons portfolio profile is more similar to Tesco than Asda,
but let’s prove it statistically.
For this, we can leverage the chi-squared test. The chi-squared is
used to evaluate whether there is significant association between the
categories of two categorical variables. In theory it is designed to
assess whether the two variables are independent of one another.
However, in our case we can view the different supermarket brands as
our variables, and we could look for the brands to have low levels of
independence compared to the Tesco distribution - as lower independence
is equal to higher similarity in our case
# convert to the correct structure
size_counts_wide <- store_size_counts %>%
select(-n) %>%
pivot_wider(names_from = retailer, values_from = percentage_stores) %>%
replace(is.na(.), 0)
target_brands <- store_size_counts %>%
select(retailer) %>%
unique()
target_brands <- target_brands[target_brands != 'Tesco']
for (brand in target_brands){
comparisons <- size_counts_wide %>%
select(Tesco, brand)
chisq <- chisq.test(comparisons)
print(brand)
print(chisq)
}
Note: Using an external vector in selections is ambiguous.
i Use `all_of(brand)` instead of `brand` to silence this message.
i See <https://tidyselect.r-lib.org/reference/faq-external-vector.html>.
This message is displayed once per session.
[1] "Asda"
Pearson's Chi-squared test
data: comparisons
X-squared = 88.809, df = 3, p-value < 2.2e-16
[1] "Morrisons"
Pearson's Chi-squared test
data: comparisons
X-squared = 13.187, df = 3, p-value = 0.004249
[1] "The Co-operative Group"
Pearson's Chi-squared test
data: comparisons
X-squared = 33.52, df = 3, p-value = 2.501e-07
From the above we want to keep in mind the null hypothesis - which is
to say that as p-value tends towards 0, the interdependence between the
two distributions (i.e. similarity in behaviour) becomes less.
Theoretically if p>=0.05 then the variables are not
interdependent.
In our case (whil not >= 0.05) we see that the p-value for
Morrisons is orders of magnitude greater than for the other brands,
meaning it has the most similar profile - as expected.
In fact, Asda has a distinctly different business model, with far
greater focus (with >50% of their stores) on large superstores (over
2800m2). Meanwhile, the Co-operative operates a set of primarily small
stores, with zero superstores in the group.
Given Morrisons has one of the lower % of overlap, and the most
similar profile, we elect to “acquire” it
# create a combined dataset
combined_stores <- major_retailers %>%
filter(retailer %in% c('Tesco', 'Morrisons'))
# now we have pretty good coverage across the UK
p <- leaflet(combined_stores) %>% addTiles() %>%
addCircles(~lng, ~lat,color = 'blue', radius = 500, opacity = .5) %>%
overlayTitle("Tesco Store Locations"); p
# save down combined dataset
write_rds(combined_stores, "data/combined_store.rds")
Identifying Which Stores to Close
Now we have a group of store locations, next we can optimise the
portfolio we will do this by opting to close stores in the same location
and then defining some logic to prioritise which stores to keep
stores
# load checkpoint dataset
combined_stores <- read_rds("data/combined_store.rds")
# split by brand
tesco_stores <- combined_stores %>%
filter(retailer == 'Tesco')
morrisons_stores <- combined_stores %>%
filter(retailer == 'Morrisons')
# convert both tables to sf standard
tesco_stores_sf <- st_as_sf(tesco_stores, coords=c('lng', 'lat'), crs="epsg:4326")
morrisons_stores_sf <- st_as_sf(morrisons_stores, coords=c('lng', 'lat'), crs="epsg:4326")
# compute distance matrix
dist_sf = st_distance(tesco_stores_sf, morrisons_stores_sf)
# note: each row represents a Tesco location and columns their distance to a Morrisons store (in meters)
M <- as.matrix(dist_sf)
M <- unclass(M)
#create binary matrix to show where Tesco store (row) within 500m of Morrisons (column)
M[] <- ifelse(M<500,1,0)
# convert to dataframe and add column to indicate Tesco index number
dist_sf <- data.frame(M)
dist_sf <- rownames_to_column(dist_sf) %>%
rename(tesco_index = rowname)
# convert from wide-form to long-form
close_store_df <- dist_sf %>%
gather(key = morrisons_index, value = flag, -c(tesco_index)) %>%
filter(flag == 1)
# clean up the morrions index column - removing the "X" from the naming convention
close_store_df <- close_store_df %>%
rowwise() %>%
mutate(morrisons_index = str_remove(morrisons_index, "X")) %>%
ungroup
# check if we have any duplicates (i.e. one store close to two others)
nrow(close_store_df)
[1] 121
# 121
n_distinct(close_store_df$tesco_index)
[1] 117
# 117
n_distinct(close_store_df$morrisons_index)
[1] 111
# 111
# join back to the Tesco master to indicate Morrisons locations
tesco_stores_df <- rownames_to_column(tesco_stores) %>%
rename(tesco_index = rowname) %>%
inner_join(close_store_df, by='tesco_index')
# join on the relevant Morrisons store data
morrisons_stores_df <- rownames_to_column(morrisons_stores) %>%
rename(morrisons_index = rowname)
store_pairs <- tesco_stores_df %>%
left_join(morrisons_stores_df, by='morrisons_index')
Now we have found all the store pairs (i.e. those that are within
500m of each other) from across the Tesco vs Morrison’s portfolio. The
next stage would be to decide the logic on which to keep stores.
In our case (and for ease of progressing with the rest of the project
notebook), we will assume that we will simply remove the Morrison’s
store, and keep the existing Tesco stores.
However, in practice you may wish to look at demographic
(specifically household income, or population density) data by postcode,
and make some assumptions around whether you want higher price goods
(usually in smaller stores) in areas of higher density or income.
remove_ids <- store_pairs$id.y
combined_cut <- combined_stores %>%
filter(!id %in% remove_ids)
nremove = nrow(combined_stores) - nrow(combined_cut)
print(paste0('number of stores removed: ', nremove))
[1] "number of stores removed: 111"
write_rds(combined_cut, "data/combined_cut.rds")
Identifying Most Appropriate Warehouse Locations
After creating a combined portfolio “post acquisition”, we will
decide where best to set up supply depots to stock the stores, which we
will achieve by first grouping stores into sensible geographic regions -
and then assign a depot to each region.
The method take will be to form clusters, based on geospatial
location that capture the closest stores, while minimising overlap. The
distribution warehouses/depots will then be located at the centroids of
the clusters.
The aim is to split the locations into c.20 regions
# load combined store set
master_stores <- read_rds("data/combined_cut.rds")
# take just the lat and lng coordinates
locs_df <- master_stores %>%
select("id","lng", "lat") %>%
column_to_rownames("id")
# set random seed to ensure repeatability of clustering
set.seed(123)
A few different clustering approaches were trialed as part of this
work: - Partition-based clustering: KMeans - Density-based clustering:
DBSCAN - Hierarchical clustering: Agglomerative
While KMeans produced good results, this methoid was discounted as it
is bad practice to use with geospatial data. This is because it assumes
coordinates are described in Euclidean coordinate system (which latitude
and longitude are not). So while it may produce reasonable results for
locations close together on Earth, it fails to account for any curvature
when clustering locations that are far away
Meanwhile, DBSCAN (which looks to cluster based on variations in
location denisty) produced strong clusters for most urban locations
(e.g. big cities), but effective grouped all less dense areas into a few
large clusters (which is ineffective for our use case)
Hierarchical clustering produced the best results - please skip to
below code segment to see
Note: While code segments for both KMeans and DBSCAN are presented
below, for interest only. To continue with the notebook, these can be
skipped, and you can jump to the Hierarchical code below
KMEANS - FOR INTEREST ONLY
# Compute k-means with k = 20 (i.e. 20 regions)
km.res <- kmeans(locs_df, 20)
# assign the cluster numbers
locs_df_km<- locs_df %>%
mutate(clust = km.res$cluster)
# print
length(unique(locs_df_km$clust))
[1] 20
# 20
# plot outputs
pal <- colorFactor(
palette = "RdYlBu",
domain = locs_df_km$clust)
p <- leaflet(locs_df_km) %>% addProviderTiles(providers$CartoDB.Positron) %>%
addCircles(~lng, ~lat,color = ~pal(clust)) %>%
overlayTitle("Store Clusters") %>%
addLegend(position = "bottomright", values = ~clust, pal = pal); p
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
# plot store count by cluster
cluster_count_km <- locs_df_km %>%
group_by(clust) %>%
tally()
ggplot(cluster_count_km, aes(x = reorder(clust, -n), y = n)) +
geom_bar(stat = "identity") + theme_minimal() +
theme(axis.text.x = element_text(angle = 0, hjust = 0.5, vjust = 0.3))

DBSCAN - FOR INTEREST ONLY
clusters <- dbscan(locs_df, eps = 0.25, minPts = 70)[['cluster']]
length(unique(clusters))
[1] 10
# 20
locs_df_db <- locs_df %>%
mutate(clust = clusters)
# plot outputs
pal <- colorFactor(
palette = "RdYlBu",
domain = locs_df_db$clust)
p <- leaflet(locs_df_db) %>% addProviderTiles(providers$CartoDB.Positron) %>%
addCircles(~lng, ~lat,color = ~pal(clust)) %>%
overlayTitle("Store Clusters") %>%
addLegend(position = "bottomright", values = ~clust, pal = pal); p
NA
HIERARCHICAL - CHOSEN APPROACH
# compute distance matrix - between location pairs
locs_sf <- st_as_sf(locs_df, coords=c('lng', 'lat'), crs="epsg:4326")
dist_sf = st_distance(locs_sf, locs_sf)
mdist <- as.matrix(dist_sf)
mdist <- unclass(mdist)
# cluster based on distance to other locations
hc <- hclust(as.dist(mdist), method="complete")
# plot dendogram
plot(hc, cex = 0.6, hang = -1)

# cluster based on defined distance separation - trialed in order to get 20 clusters
d <- 215000
locs_df_hc <- locs_df %>%
mutate(clust = cutree(hc, h=d))
# print
length(unique(locs_df_hc$clust))
[1] 20
# 20
# plot outputs
pal <- colorFactor(
palette = "RdYlBu",
domain = locs_df_hc$clust)
p <- leaflet(locs_df_hc) %>% addProviderTiles(providers$CartoDB.Positron) %>%
addCircles(~lng, ~lat,color = ~pal(clust)) %>%
overlayTitle("Store Clusters") %>%
addLegend(position = "bottomright", values = ~clust, pal = pal); p
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
When viewing on a map, it appears to be an initially very sensible
clustering. However, there looks to be some very small clusters, which
it may not be logical to treat as a cluster in and of itself.
We explore this further by looking at the spread of store counts in
each
# plot store count by cluster
cluster_count_hc <- locs_df_hc %>%
group_by(clust) %>%
tally()
ggplot(cluster_count_hc, aes(x = reorder(clust, -n), y = n)) +
geom_bar(stat = "identity") + theme_minimal() +
theme(axis.text.x = element_text(angle = 0, hjust = 0.5, vjust = 0.3))

# plot store count by cluster as %
cluster_count_hc <- cluster_count_hc %>%
mutate(percentage = (n / nrow(locs_df_hc))*100)
ggplot(cluster_count_hc, aes(x = reorder(clust, -percentage), y = percentage)) +
geom_bar(stat = "identity") + theme_minimal() +
theme(axis.text.x = element_text(angle = 0, hjust = 0.5, vjust = 0.3))

From the above chart, it’s obvious that there are clusters with few
store locations. Plotting these on the map (e.g. below code segment),
shows that these tend to be in isolated locations - e.g. Shetland
Islands, Hebrides, Orkney Islands, or Guernsey & Jersey
All of these have <1% of the total store locations (which is how
we will decide to remove them)
# plot outputs - exploratory only
cluster_number <- 18
sample_cluster <- locs_df_hc %>%
filter(clust == cluster_number)
p <- leaflet(sample_cluster) %>% addProviderTiles(providers$CartoDB.Positron) %>%
addCircles(~lng, ~lat,color = ~pal(clust)) %>%
overlayTitle("Store Clusters") %>%
addLegend(position = "bottomright", values = ~clust, pal = pal); p
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
We will remove such remote locations from our analysis and
re-cluster
# remove those with less than 1%
remote_clusters <- cluster_count_hc %>%
filter(percentage < 1) %>%
select(clust)
locs_df_hc_cut <- locs_df_hc %>%
filter(!clust %in% remote_clusters$clust)
# compute distance matrix - between location pairs
locs_sf <- st_as_sf(locs_df_hc_cut %>% select(lng, lat), coords=c('lng', 'lat'), crs="epsg:4326")
dist_sf = st_distance(locs_sf, locs_sf)
mdist <- as.matrix(dist_sf)
mdist <- unclass(mdist)
# plot dendogram
hc <- hclust(as.dist(mdist), method="complete")
plot(hc, cex = 0.6, hang = -1)

# cluster based on defined distance separation - trial and erro to get reasonable clusters
d <- 175000
locs_df_hc_cut <- locs_df_hc_cut %>%
mutate(clust2 = cutree(hc, h=d))
# print
length(unique(locs_df_hc_cut$clust2))
[1] 20
# 20
# plot outputs
pal <- colorFactor(
palette = "RdYlBu",
domain = locs_df_hc_cut$clust2)
p <- leaflet(locs_df_hc_cut) %>% addProviderTiles(providers$CartoDB.Positron) %>%
addCircles(~lng, ~lat,color = ~pal(clust2)) %>%
overlayTitle("Store Clusters") %>%
addLegend(position = "bottomright", values = ~clust2, pal = pal); p
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Now that we have sensible clusters forming, the next step is to
determine the location of the warehouses/depots that will service each
region. For this, I will use simply the centroids - which can be
computed using the mean lat,lng values
# compute the centre of each cluster and plot this
cluster_centroids <- locs_df_hc_cut %>%
group_by(clust2) %>%
summarize(mean_lng = mean(lng, na.rm=TRUE), mean_lat = mean(lat, na.rm=TRUE))
p <- leaflet(locs_df_hc_cut) %>% addProviderTiles(providers$CartoDB.Positron) %>%
addCircles(~lng, ~lat,color = ~pal(clust2)) %>%
overlayTitle("Store Clusters") %>%
addLegend(position = "bottomright", values = ~clust2, pal = pal) %>%
addMarkers(data = cluster_centroids, ~mean_lng, ~mean_lat, label = ~as.character(clust2)); p
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Now we have the warehouse locations, and some initial clusters.
However, we will see that in some cases the hierarchical clustering does
not result in sensible assignments. For example, there are situations
where bodies of water exist that simply clustering by coordinates (lat,
lng) will be unable to take account of.
e.g. in the below, we can see that a location is on the other side of
the estuary
cluster_number <- 8
sample_cluster <- locs_df_hc_cut %>%
filter(clust2 == cluster_number)
sample_centroid <- cluster_centroids %>%
filter(clust2 == cluster_number)
p <- leaflet(sample_cluster) %>% addProviderTiles(providers$CartoDB.Positron) %>%
addCircles(~lng, ~lat,color = ~pal(clust)) %>%
overlayTitle("Store Clusters") %>%
addLegend(position = "bottomright", values = ~clust2, pal = pal) %>%
addMarkers(data = sample_centroid, ~mean_lng, ~mean_lat); p
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
In practice this means that the drive distance required from the
depot to restock the store is far greater than initially anticipated.
Furthermore, there may be a nearer depot (belonging to another cluster)
that is better suited for restocking this store.
For this we will need to understand the drive time and reassign
locations to their nearest cluster centre based on this.
# create a checkpoint
locs_df_hc_cut <- locs_df_hc_cut %>%
select(-clust) %>%
rename(cluster = clust2)
cluster_centroids <- cluster_centroids %>%
rename(lat=mean_lat, lng=mean_lng, cluster = clust2)
write_rds(locs_df_hc_cut, "data/cut_store_locs.rds")
write_rds(cluster_centroids, "data/warehouse_locations.rds")
Optimise Based on Drive Time
For this part, we will leverage the brilliant Project OSRM (Open
Source Routing Machine), which can be found here: https://github.com/Project-OSRM/osrm-backend
It will leverage docker and allow us to run a local app, which we can
send API request to in order determine the drive distance and time
between location pairs.
For further details on installation and running see the
“install_osrm_docker.Rmd” file.
store_locations <- read_rds("data/cut_store_locs.rds")
warehouse_locations <- read_rds("data/warehouse_locations.rds")
# create the string of warehouse locations
coords <- c()
for (row in 1:nrow(warehouse_locations)){
warehouse <- paste(warehouse_locations$lng[row], warehouse_locations$lat[row], sep=",")
coords <- append(coords, warehouse)
}
warehouse_string <- paste(coords, collapse=";")
# iterate through each store and find the nearest warehouse/depot
cluster_vector <- c()
for (row in 1:nrow(store_locations)){
location <- store_locations %>%
select(lng, lat) %>%
slice(row) %>%
paste(collapse=",")
response <- GET(paste0("http://127.0.0.1:5000/table/v1/driving/", location, ";", warehouse_string, "?sources=0"))
result <- content(response, as='parsed')
duration_matrix <- result$durations[[1]][-1]
nearest_warehouse <- warehouse_locations %>%
mutate(store_travel_duration = as.numeric(duration_matrix)) %>%
slice(which.min(.$store_travel_duration)) %>%
pull(cluster)
cluster_vector <- append(cluster_vector, nearest_warehouse)
if (row%%10 == 0){
print(paste0(row,"/",nrow(store_locations)))
}
}
store_locations_updated <- store_locations %>%
mutate(updated_cluster = cluster_vector) %>%
rename(original_cluster = cluster) %>%
mutate(same_cluster = case_when(
original_cluster == updated_cluster ~ 1,
original_cluster != updated_cluster ~ 0
))
write_rds(store_locations_updated, "data/master_store_df.rds")
Now we can re-plot the updated cluster assignments to check that
previous issues have been addressed
store_locations_updated <- read_rds("data/master_store_df.rds")
warehouse_locations <- read_rds("data/warehouse_locations.rds")
# plot the new clusters
pal <- colorFactor(
palette = "RdYlBu",
domain = store_locations_updated$updated_cluster)
p <- leaflet(store_locations_updated) %>% addProviderTiles(providers$CartoDB.Positron) %>%
addCircles(~lng, ~lat,color = ~pal(updated_cluster), label = ~as.character(updated_cluster)) %>%
overlayTitle("Store Clusters") %>%
addLegend(position = "bottomright", values = ~updated_cluster, pal = pal) %>%
addMarkers(data = warehouse_locations, ~lng, ~lat, label = ~as.character(cluster)); p
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Assigning Master vs Sub Warehouses/Depots
The last thing we will do is to decide the appropriate mix of main
warehouses/depots, and satellites/subs. We will aim for just 5 master
depots, with 15 sub depots for servicing regions.
For this we will look at the store count assigned to each cluster
(with the idea being to assign main depots as those with most associated
stores), and the geographic spread (to ensure our main depots offer
adequate coverage across the country).
This part is a slightly manual process.
cluster_count <- store_locations_updated %>%
group_by(updated_cluster) %>%
tally()
ggplot(cluster_count, aes(x = reorder(updated_cluster, -n), y = n)) +
geom_bar(stat = "identity") + theme_minimal() +
theme(axis.text.x = element_text(angle = 0, hjust = 0.5, vjust = 0.3))

top_7 <- c(12, 3, 11, 15, 1, 6, 5, 2, 16)
top_7_clusters <- store_locations_updated %>%
filter(updated_cluster %in% top_7)
pal <- colorFactor(
palette = "RdYlBu",
domain = top_7_clusters$updated_cluster)
p <- leaflet(top_7_clusters) %>% addProviderTiles(providers$CartoDB.Positron) %>%
addCircles(~lng, ~lat,color = ~pal(updated_cluster)) %>%
overlayTitle("Store Clusters") %>%
addLegend(position = "bottomright", values = ~updated_cluster, pal = pal) %>%
addMarkers(data = warehouse_locations, ~lng, ~lat, label = ~as.character(cluster)); p
- London: Unsurprisingly London, with such a high population, captures
the two largest clusters (3 & 12). Given the size of these clusters,
we will take both as master depots
- Manchester: The third biggest cluster (11) is located in Manchester.
Given the ability to service the second largest number of stores, as
well as the norther regions, it is reasonable to also take this as a
master depot.
- Birmingham: The decision for the next master depot, becomes a weigh
up between clusters 1 & 15. However, given the proximity of cluster
15 to Manchester, I chose to select location 1 as a master depot, given
it can be used to service service Wales and the Midlands, with the East
of England being serviced out of Manchester or London as
appropriate.
- Glasgow: Lastly, it is also reasonable to assume a northern depot in
Scotland to service the country, North of England, and Northern Ireland
sub-depots. Glasgow (cluster 14) has the highest number of stores and is
well located to service Northern Ireland on the west coast.
From manual investigation, and some business logic we have assigned
our 5 master depots. However, it is worth noting that this could be done
using driving distance (e.g. pick 5 locations that minimise aggregated
drive time to reach all sub-depots), or some alternative business logic.
This would be an area of possible improvement.
# plot the final map
master_depots <- c(1, 3, 11, 12, 14)
warehouse_locations <- warehouse_locations %>%
mutate(tier = ifelse(cluster %in% master_depots, 'master', 'sub')) %>%
mutate(icon_colour = ifelse(tier == "master", "red", "blue"))
icons <- awesomeIcons(iconColor = "black",
library = "ion",
markerColor = warehouse_locations$icon_colour
)
pal <- colorFactor(
palette = "RdYlBu",
domain = store_locations_updated$updated_cluster)
p <- leaflet(store_locations_updated) %>% addProviderTiles(providers$CartoDB.Positron) %>%
addCircles(~lng, ~lat,color = ~pal(updated_cluster)) %>%
overlayTitle("Store Clusters") %>%
addLegend(position = "bottomright", values = ~updated_cluster, pal = pal) %>%
addAwesomeMarkers(data = warehouse_locations, ~lng, ~lat, icon = icons, label = ~as.character(cluster)); p
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Warning: n too large, allowed maximum for palette RdYlBu is 11
Returning the palette you asked for with that many colors
Potential Improvements
Vehicle Routing:
The next step would be to determine the most optimal driving routes
for trucks to leave each of the master depots to service the sub depots
- i.e what is the most efficient method of trucks, leaving each of the
the master depots, to service the sub-depots in our network.
If we assume, given the size of deliveries, it is realistic to have a
single truck per sub-depot restock, the problem becomes simply a
shortest duration calculation from each sub-depot to the master depots -
which OSRM docker can handle.
However, if you want just one truck per depot to be responsible for
restocking each of the associated sub-depots, then we need to frame the
problem as a “travelling salesman problem” - i.e. what is the shortest
route for a single truck to visit all the necessary stops just once and
return home to the master depot? For this, I would recommend exploring
the VROOM Project (Vehicle Routing Open-Source Optimisation Machine): https://github.com/VROOM-Project
Master Depot Selection:
As mentioned above, another potential improvement could be to select
the location of the master depots, based not on the number of locations
and manual business logic, but based on which 5 locations minimise the
drive time to each of the remaining sub-depots. For this we could use
OSRM docker also.
Cluster Pruning:
While arlier on we disregarded clusters that were extremely remote,
there are still a few locations contained in our clusters that are
rather remote, with the cluster being fairly spread (e.g. North East
Scotland).
To address this, we could iteratively remove locations from our
dataset that are a certain distance from the nearest cluster centre, and
then perform reclustering. The intention would be to repeatedly “prune”
the most remote locations in between rounds of clustering.
This would result in more densely formed regions, but at the expense
of excluding certain locations from our logistics network.
LS0tDQp0aXRsZTogIlN1cGVybWFya2V0X1N0b3JlX0FuYWx5dGljcyINCmF1dGhvcjogInRvdG9nb3QiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQojI0ludHJvZHVjdGlvbg0KVGhpcyBub3RlYm9vayBsZXZlcmFnZXMgMjB0aCBFZGl0aW9uIG9mIEdlb2x5dGl4IE9wZW4gU3VwZXJtYXJrZXQgUmV0YWlsIFBvaW50cyBkYXRhIHNldCB0byBwZXJmb3JtIHNvbWUgYW5hbHl0aWNzIG9uIGdlb3NwYXRpYWwgZGF0YS4NCg0KVGhlIHBlcmNlaXZlZCB0YXNrIGF0IGhhbmQgYXNzdW1lcyBhIGxhcmdlIHN1cGVybWFya2V0IGNoYWluIGluIHRoZSBVSyAoVGVzY28pIGlzIGludGVyZXN0aW5nIGluIGFjcXVpcmluZyBhIGNvbXBldGl0b3IgY2hhaW4sIGZvbGxvd2luZyB3aGljaCB0aGUgY2hhbGxlbmdlIGlzIHRvIGlkZW50aWZ5IHJlYXNvbmFibGUgZ2VvZ3JhcGhpYyByZWdpb25zIGludG8gd2hpY2ggdG8gc2VnbWVudCBoZSBzdG9yZXMsIGFuZCBtb3N0IGFwcHJvcHJpYXRlIGxvY2F0aW9ucyB0byBwb3NpdGlvbiB3YXJlaG91c2UgZGVwb3RzIGZvciBzdG9ja2luZyB0aGUgc3RvcmVzLg0KDQpUaGUgYXBwcm9hY2ggdGFrZW4gbWFrZXMgdXNlIG9mIGNsdXN0ZXJpbmcgdGVjaG5pcXVlcywgYXMgd2VsbCBhcyB0aGlyZCBwYXJ0IGFwcGxpY2F0aW9ucyAocnVuIG9uIERvY2tlcikgdG8gYXJyaXZlIGF0IGEgc29sdXRpb24uDQoNClRoZSBpbnRlbnRpb24gaXMgdG8gcHJvdmlkZSBhbiBleGFtcGxlIG9mIGhvdyB0byB3b3JrIHdpdGggZ2Vvc3BhdGlhbCBkYXRhLCBpbiBhIHdheSB0aGF0IGNvdWxkIGJlIGFwcGxpY2FibGUgdG8gYSByYW5nZSBvZiBkaWZmZXJlbnQgaW5kdXN0cmllcyAtIGZyb20gbG9naXN0aWNzIHRvIG1hcmtldGluZyAtIGFuZCBzZWN0b3JzIC0gZnJvbSByZXRhaWwgdG8gaGVhbHRoY2FyZS4NCg0KDQoNCiMjSW5zdGFsbCBQYWNrYWdlcw0KYGBge3J9DQojaW5zdGFsbC5wYWNrYWdlcygibGVhZmxldCIpDQojaW5zdGFsbC5wYWNrYWdlcygidGlkeXZlcnNlIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJsZWFmbGV0IikNCiNpbnN0YWxsLnBhY2thZ2VzKCJodHRyIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJ3cml0ZXhsIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJyZWFkeGwiKQ0KI2luc3RhbGwucGFja2FnZXMoImxlYWZsZXQiKQ0KI2luc3RhbGwucGFja2FnZXMoImdlb3NwaGVyZSIpDQojaW5zdGFsbC5wYWNrYWdlcygic2YiKQ0KI2luc3RhbGwucGFja2FnZXMoImNsdXN0ZXIiKQ0KI2luc3RhbGwucGFja2FnZXMoImZhY3RvZXh0cmEiKQ0KI2luc3RhbGwucGFja2FnZXMoImRlbmRleHRlbmQiKQ0KI2luc3RhbGwucGFja2FnZXMoImRic2NhbiIpDQojaW5zdGFsbC5wYWNrYWdlcygiY3VybCIpDQoNCmxpYnJhcnkoaHR0cikNCmxpYnJhcnkocmVhZHhsKQ0KbGlicmFyeShnZ3Bsb3QyKQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KGxlYWZsZXQpDQpsaWJyYXJ5KGh0bWx0b29scykNCmxpYnJhcnkoZ2Vvc3BoZXJlKQ0KbGlicmFyeShzZikNCmxpYnJhcnkoY2x1c3RlcikNCmxpYnJhcnkoZmFjdG9leHRyYSkNCmxpYnJhcnkoZGVuZGV4dGVuZCkNCmxpYnJhcnkoZGJzY2FuKQ0KbGlicmFyeShjdXJsKQ0KYGBgDQoNCg0KDQojIyBEZWZpbmUgRnVuY3Rpb25zDQpgYGB7cn0NCiMgYWRkIGxlYWZsZXQgdGl0bGUNCm92ZXJsYXlUaXRsZSA8LSBmdW5jdGlvbihwbG90LCB0ZXh0KSB7DQogIA0KICB0YWcubWFwLnRpdGxlIDwtIHRhZ3Mkc3R5bGUoSFRNTCgiDQogIC5sZWFmbGV0LWNvbnRyb2wubWFwLXRpdGxlIHsgDQogICAgdHJhbnNmb3JtOiB0cmFuc2xhdGUoLTUwJSwyMCUpOw0KICAgIHBvc2l0aW9uOiBmaXhlZCAhaW1wb3J0YW50Ow0KICAgIGxlZnQ6IDUwJTsNCiAgICB0ZXh0LWFsaWduOiBjZW50ZXI7DQogICAgcGFkZGluZy1sZWZ0OiAxMHB4OyANCiAgICBwYWRkaW5nLXJpZ2h0OiAxMHB4OyANCiAgICBiYWNrZ3JvdW5kOiByZ2JhKDI1NSwyNTUsMjU1LDAuNzUpOw0KICAgIGZvbnQtd2VpZ2h0OiByZWd1bGFyOw0KICAgIGZvbnQtc2l6ZTogMThweDsNCiAgfQ0KICAiKSkNCiAgDQogIHRpdGxlIDwtIHRhZ3MkZGl2KA0KICAgIHRhZy5tYXAudGl0bGUsIEhUTUwodGV4dCkNCiAgKQ0KICBwbG90ICU+JSBhZGRDb250cm9sKHRpdGxlLCBwb3NpdGlvbiA9ICJ0b3BsZWZ0IiwgY2xhc3NOYW1lID0gIm1hcC10aXRsZSIpDQogIA0KfQ0KDQoNCiMgY29tcHV0ZSBkaXN0YW5jZSBmcm9tIHR3byBjb29yZHMNCmNhbGNIYXZlcnNpbmUgPC0gZnVuY3Rpb24obG9jMSwgbG9jMikgew0KICANCiAgZGYgPC0gbWF0cml4KGMobG9jMVsxXSwgbG9jMlsxXSwgbG9jMVsyXSwgbG9jMlsyXSksIG5yb3cgPSAyKQ0KICBkaXN0IDwtIGRpc3RIYXZlcnNpbmUoZGYpDQogIHJldHVybihkaXN0KQ0KfQ0KDQoNCiMgY29tcHV0ZSB0aGUgbnVtYmVyIG9mIGxvY2F0aW9ucyB3aXRoaW4gNTAwbQ0KY2FsY0Nsb3NlU3RvcmUgPC0gZnVuY3Rpb24obWFzdGVyX3N0b3JlcywgdGFyZ2V0X3N0b3Jlcykgew0KICANCiAgIyBjb252ZXJ0IGJvdGggdGFibGVzIHRvIHNmIHN0YW5kYXJkDQogIG1hc3Rlcl9zdG9yZXNfc2YgPC0gc3RfYXNfc2YobWFzdGVyX3N0b3JlcywgY29vcmRzPWMoJ2xuZycsICdsYXQnKSwgY3JzPSJlcHNnOjQzMjYiKQ0KICB0YXJnZXRfc3RvcmVzX3NmIDwtIHN0X2FzX3NmKHRhcmdldF9zdG9yZXMsIGNvb3Jkcz1jKCdsbmcnLCAnbGF0JyksIGNycz0iZXBzZzo0MzI2IikNCiAgDQogICMgY29tcHV0ZSBkaXN0YW5jZSBtYXRyaXgNCiAgIyBlYWNoIHJvdyByZXByZXNlbnRzIGEgdGFyZ2V0IGxvY2F0aW9uIGFuZCBjb2x1bW5zIHRoZWlyIGRpc3RhbmNlIHRvIGEgbWFzdGVyIHN0b3JlIChpbiBtZXRlcnMpDQogIGRpc3QgPSBzdF9kaXN0YW5jZSh0YXJnZXRfc3RvcmVzX3NmLCBtYXN0ZXJfc3RvcmVzX3NmKQ0KICANCiAgIyBjb252ZXJ0IHRvIGRhdGFmcmFtZSBhbmQgY29tcHV0ZSB0aGUgcm93IG1pbiAod2Ugb25seSBjYXJlIGlmIGl0IGlzIHdpdGhpbiA1MDBtIG5vdCBob3cgb2Z0ZW4pDQogIGRpc3QgPC0gZGF0YS5mcmFtZShkaXN0KQ0KICBkaXN0JG1pbiA8LSBhcHBseShkaXN0W10sIE1BUkdJTiA9ICAxLCBGVU4gPSBtaW4pDQogIA0KICAjIGNvbXB1dGUgbnVtYmVyIG9mIHN0b3JlcyB3aXRoaW4gNTAwbSBvZiBhIG1hc3RlciBicmFuZCBzdG9yZQ0KICBkaXN0X2Nsb3NlIDwtIGRpc3QgJT4lIA0KICAgIGZpbHRlcihtaW4gPD01MDApDQogIA0KICBjbG9zZV9zdG9yZXMgPC0gbnJvdyhkaXN0X2Nsb3NlKQ0KICANCiAgcmV0dXJuKGNsb3NlX3N0b3JlcykNCn0NCmBgYA0KDQoNCg0KIyMgTWFpbiBSdW4NCmBgYHtyfQ0KIyBsb2FkIHN0YXJ0aW5nIGRhdGENCmFsbF9zdG9yZXMgPC0gcmVhZF94bHMoJ2RhdGEvR0VPTFlUSVggLSBVSyBSZXRhaWxQb2ludHMvdWtfZ2x4X29wZW5fcmV0YWlsX3BvaW50c192MjRfMjAyMjA2LnhscycpDQoNCiMgcGxvdCBzdG9yZSBjb3VudCBieSByZXRhaWxlcg0KcmV0YWlsZXJfY291bnQgPC0gYWxsX3N0b3JlcyAlPiUNCiAgZ3JvdXBfYnkocmV0YWlsZXIpICU+JQ0KICB0YWxseSgpDQoNCmdncGxvdChyZXRhaWxlcl9jb3VudCwgYWVzKHggPSByZW9yZGVyKHJldGFpbGVyLCAtbiksIHkgPSBuKSkgKw0KICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IikgKyB0aGVtZV9taW5pbWFsKCkgKw0KICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDkwLCBoanVzdCA9IDEsIHZqdXN0ID0gMC4zKSkNCg0KIyBwbG90IHN0b3JlIGNvdW50IGJ5IGNsdXN0ZXIgYXMgJQ0KcmV0YWlsZXJfY291bnQgPC0gcmV0YWlsZXJfY291bnQgJT4lDQogIG11dGF0ZShwZXJjZW50YWdlID0gKG4gLyBzdW0obikpKjEwMCkNCg0KZ2dwbG90KHJldGFpbGVyX2NvdW50LCBhZXMoeCA9IHJlb3JkZXIocmV0YWlsZXIsIC1wZXJjZW50YWdlKSwgeSA9IHBlcmNlbnRhZ2UpKSArDQogIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIHRoZW1lX21pbmltYWwoKSArDQogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gOTAsIGhqdXN0ID0gMSwgdmp1c3QgPSAwLjMpKQ0KYGBgDQoNCmBgYHtyfQ0KIyBjcmVhdGUgbGlzdCBvZiBsYXJnZXN0IHN0b3Jlcw0KbWFqb3JzIDwtIGMoIkFsZGkiLCAiQXNkYSIsICJMaWRsIiwgIk1hcmtzIGFuZCBTcGVuY2VyIiwgIk1vcnJpc29ucyIsIA0KICAgICAgICAgICAgIlNhaW5zYnVyeXMiLCAiVGVzY28iLCAiVGhlIENvLW9wZXJhdGl2ZSBHcm91cCIsICJXYWl0cm9zZSIpDQoNCiMgY29tcHV0ZSAlIG9mIHN0b3JlcyBjYXB0dXJlZCBieSBtYWpvciBjaGFpbnMNCnByaW50KHJldGFpbGVyX2NvdW50ICU+JSBmaWx0ZXIocmV0YWlsZXIgJWluJSBtYWpvcnMpICU+JSBwdWxsKHBlcmNlbnRhZ2UpICU+JSBzdW0pDQojIDY2Ljk3NSAlDQojIHRoZSBtYWpvciBjaGFpbnMgY2FwdHVyZSAyLzNyZHMgb2YgdGhlIHVuaXZlcnNlDQoNCiMgcGxvdCBsb2NhdGlvbiBvZiBtYWpvciBjaGFpbnMNCm1ham9yX3JldGFpbGVycyA8LSBhbGxfc3RvcmVzICU+JSBmaWx0ZXIoYHJldGFpbGVyYCAlaW4lIG1ham9ycykgJT4lDQogIHNlbGVjdChpZCwgcmV0YWlsZXIsIHBvc3Rjb2RlLCBsbmcgPSBsb25nX3dncywgbGF0ID0gbGF0X3dncywgc2l6ZV9iYW5kKQ0KDQpwYWwgPC0gY29sb3JGYWN0b3IoDQogIHBhbGV0dGUgPSBjKA0KICAgICJjeWFuIiwgImdyZWVuIiwgInB1cnBsZSIsICJibGFjayIsICJ5ZWxsb3ciLCAib3JhbmdlIiwgImJsdWUiLCAidHVycXVvaXNlIiwgInJlZCIgDQogICAgKSwNCiAgZG9tYWluID0gbWFqb3JfcmV0YWlsZXJzJHJldGFpbGVyKQ0KDQpwIDwtIGxlYWZsZXQobWFqb3JfcmV0YWlsZXJzKSAlPiUgYWRkVGlsZXMoKSAlPiUNCiAgYWRkQ2lyY2xlcyh+bG5nLCB+bGF0LGNvbG9yID0gfnBhbChyZXRhaWxlcikpICU+JQ0KICBvdmVybGF5VGl0bGUoIk1ham9yIFJldGFpbGVyIFN0b3JlIExvY2F0aW9ucyIpICU+JQ0KICBhZGRMZWdlbmQocG9zaXRpb24gPSAiYm90dG9tcmlnaHQiLCB2YWx1ZXMgPSB+cmV0YWlsZXIsIHBhbCA9IHBhbCk7IHANCmBgYA0KDQojIyMgUHVyY2hhc2Ugb2YgQ29tcGV0aXRvcg0KDQpBc3N1bWluZyB3ZSBhcmUgVGVzY28gKGFzIHRoZSBsYXJnZXN0IHN1cGVybWFya2V0KSBhbmQgYXJlIGxvb2tpbmcgdG8gYWNxdWlyZSBvbmUgb2YgdGhlIG90aGVyIGNoYWlucywgd2UgY291bGQgbmVlZCB0byBzZXQgYSBzZXJpZXMgb2YgY29uZGl0aW9ucyB1bmRlciB3aGljaCB3ZSB3b3VsZCBhY3F1aXJlIGEgY29tcGV0aXRvci4gSW4gdGhpcyBleGFtcGxlLCBsZXQncyBpbWFnaW5lIHRoZXNlIGFyZSBzaW1wbHk6DQoNCjEuIEFjcXVpcmUgYmFzZWQgb24gd2hpY2ggY2hhaW4gb2ZmZXJzIHRoZSAiYmVzdCBjb3ZlcmFnZSItIHdoZXJlIHRoZXJlIGlzIG1pbmltdW0gb3ZlcmxhcCB3aXRoIGV4aXN0aW5nIFRlc2NvIGxvY2F0aW9ucy4gaS5lLiBmaW5kIHRoZSBjb21wZXRpdG9yIHdoZXJlIGxlYXN0ICUgb2Ygc3RvcmVzIHdpdGhpbiBhIGNlcnRhaW4gcmFkaXVzIG9mIFRlc2NvIHN0b3Jlcw0KDQoyLiBPZiB0aGUgc3RvcmVzIHdpdGggbWluaW1hbCBvdmVybGFwIGluIGNvdmVyYWdlLCB3aGljaCBoYXZlIGEgcHJvZmlsZSBvZiBzdG9yZSBsb2NhdGlvbnMgdGhhdCBiZXN0IG1hdGNoZXMgVGVzY28ncyBidXNpbmVzcyBzdHJhdGVneQ0KYGBge3J9DQojIHdlIHdpbGwgc2V0IGEgY3V0LW9mZiBvZiA1MDBtIGVhY2ggb3RoZXIgLSBiYXNlZCBvbiBHcmVhdCBDaXJjbGUgZGlzdGFuY2UNCm1ham9yX3JldGFpbGVycyA8LSBtYWpvcl9yZXRhaWxlcnMgJT4lIA0KICByb3d3aXNlICU+JQ0KICBtdXRhdGUoY29vcmRpbmF0ZXMgPSBsaXN0KGMobG5nLCBsYXQpKSkgJT4lDQogIHVuZ3JvdXANCg0KIyBpc29sYXRlIHRoZSB0ZXNjbyBsb2NhdGlvbnMNCnRlc2NvX2xvYyA8LSBtYWpvcl9yZXRhaWxlcnMgJT4lDQogIGZpbHRlcihyZXRhaWxlciA9PSAiVGVzY28iKQ0KDQojIG9idGFpbiBsaXN0IG9mIGFsbCBvdGhlciB0YXJnZXQgYnJhbmRzDQp0YXJnZXRzIDwtIG1ham9yc1ttYWpvcnMgIT0gJ1Rlc2NvJ10NCg0KIyBpdGVyYXRlIGFuZCBjb21wdXRlICUgb2YgZWFjaCBicmFuZCB3aXRoaW4gNTAwbQ0KZm9yIChicmFuZCBpbiB0YXJnZXRzKXsNCiAgDQogIHRhcmdldF9sb2MgPC0gbWFqb3JfcmV0YWlsZXJzICU+JQ0KICAgIGZpbHRlcihyZXRhaWxlciA9PSBicmFuZCkNCiAgDQogIGNsb3NlX3N0b3JlcyA8LSBjYWxjQ2xvc2VTdG9yZSh0ZXNjb19sb2MsIHRhcmdldF9sb2MpDQogIA0KICBwcmludChicmFuZCkNCiAgcHJpbnQoKGNsb3NlX3N0b3Jlcy9ucm93KHRhcmdldF9sb2MpKSoxMDApDQp9DQojIC0tLS0tLXJlc3VsdHMNCiMgIkFsZGkiDQojIDI2LjM0Mjk4DQojICJBc2RhIg0KIyAxNC4yODU3MQ0KIyAiTGlkbCINCiMgMjkuNDE3ODgNCiMgIk1hcmtzIGFuZCBTcGVuY2VyIg0KIyAzMy45ODg3Ng0KIyAiTW9ycmlzb25zIg0KIyAxMi4zMTk2NA0KIyAiU2FpbnNidXJ5cyINCiMgMzMuMzA5NjYNCiMgIlRoZSBDby1vcGVyYXRpdmUgR3JvdXAiDQojIDE1LjQ1MDQ4DQojICJXYWl0cm9zZSINCiMgMzUuMTgwMDYNCmBgYA0KDQpGcm9tIHRoZSBhYm92ZSByZXN1bHRzIHdlIHNlZSB0aGF0IE1vcnJpc29ucyBhbmQgQXNkYSBoYXZlIHRoZSBsb3dlc3QgJS4gV2UgY2FuIG5vdyBleHBsb3JlIHRoZSBwcm9maWxlIG9mIHRoZWlyIHJlc3BlY3RpdmUgc3RvcmUgcG9ydGZvbGlvcyAodG8gc2VlIHRoZSBudW1iZXIgb2Ygc3RvcmVzIHRoZXNlIGJyYW5kcyBvZmZlciBieSBzaXplKQ0KYGBge3J9DQpwcmludChyZXRhaWxlcl9jb3VudCAlPiUgZmlsdGVyKHJldGFpbGVyICVpbiUgYygnQXNkYScsICdNb3JyaXNvbnMnLCAnVGhlIENvLW9wZXJhdGl2ZSBHcm91cCcpKSkNCiMgQXNkYSAgICAgICAgNjM3DQojIE1vcnJpc29ucyAgIDkwMQ0KIyBDb29wICAgICAgICAyNjg2DQoNCiMgYXMgd2VsbCBhcyB0aGUgYXJyYXkgb2Ygc3RvcmUgc2l6ZXMgLSBhbmQgaG93IHRoaXMgY29tcGFyZXMgdG8gVGVzY28NCnN0b3JlX3NpemVfY291bnRzIDwtIGFsbF9zdG9yZXMgJT4lDQogIGZpbHRlcihyZXRhaWxlciAlaW4lIGMoJ1Rlc2NvJywgJ0FzZGEnLCAnTW9ycmlzb25zJywgJ1RoZSBDby1vcGVyYXRpdmUgR3JvdXAnKSkgJT4lDQogIGdyb3VwX2J5KHJldGFpbGVyLCBzaXplX2JhbmQpICU+JQ0KICB0YWxseSgpICU+JQ0KICBncm91cF9ieShyZXRhaWxlcikgJT4lDQogIG11dGF0ZShwZXJjZW50YWdlX3N0b3JlcyA9IChuIC8gc3VtKG4pKSoxMDApDQoNCg0KZ2dwbG90KHN0b3JlX3NpemVfY291bnRzLCBhZXMoeCA9IHNpemVfYmFuZCwgeSA9IHBlcmNlbnRhZ2Vfc3RvcmVzLCBmaWxsPXJldGFpbGVyKSkgKw0KICBnZW9tX2NvbChwb3NpdGlvbiA9ICJkb2RnZSIpICsgDQogIHRoZW1lX21pbmltYWwoKSArDQogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gOTAsIGhqdXN0ID0gMSwgdmp1c3QgPSAwLjMpKQ0KYGBgDQpGcm9tIGdsYW5jaW5nIGF0IHRoZSBjaGFydHMsIGl0IGlzIGZhaXJseSBvYnZpb3VzIG9uIGZpcnN0IGluc3BlY3Rpb24gdGhhdCdzIE1vcnJpc29ucyBwb3J0Zm9saW8gcHJvZmlsZSBpcyBtb3JlIHNpbWlsYXIgdG8gVGVzY28gdGhhbiBBc2RhLCBidXQgbGV0J3MgcHJvdmUgaXQgc3RhdGlzdGljYWxseS4NCg0KRm9yIHRoaXMsIHdlIGNhbiBsZXZlcmFnZSB0aGUgY2hpLXNxdWFyZWQgdGVzdC4gVGhlIGNoaS1zcXVhcmVkIGlzIHVzZWQgdG8gZXZhbHVhdGUgd2hldGhlciB0aGVyZSBpcyBzaWduaWZpY2FudCBhc3NvY2lhdGlvbiBiZXR3ZWVuIHRoZSBjYXRlZ29yaWVzIG9mIHR3byBjYXRlZ29yaWNhbCB2YXJpYWJsZXMuIEluIHRoZW9yeSBpdCBpcyBkZXNpZ25lZCB0byBhc3Nlc3Mgd2hldGhlciB0aGUgdHdvIHZhcmlhYmxlcyBhcmUgaW5kZXBlbmRlbnQgb2Ygb25lIGFub3RoZXIuDQoNCkhvd2V2ZXIsIGluIG91ciBjYXNlIHdlIGNhbiB2aWV3IHRoZSBkaWZmZXJlbnQgc3VwZXJtYXJrZXQgYnJhbmRzIGFzIG91ciB2YXJpYWJsZXMsIGFuZCB3ZSBjb3VsZCBsb29rIGZvciB0aGUgYnJhbmRzIHRvIGhhdmUgbG93IGxldmVscyBvZiBpbmRlcGVuZGVuY2UgY29tcGFyZWQgdG8gdGhlIFRlc2NvIGRpc3RyaWJ1dGlvbiAtIGFzIGxvd2VyIGluZGVwZW5kZW5jZSBpcyBlcXVhbCB0byBoaWdoZXIgc2ltaWxhcml0eSBpbiBvdXIgY2FzZQ0KDQpgYGB7cn0NCiMgY29udmVydCB0byB0aGUgY29ycmVjdCBzdHJ1Y3R1cmUNCnNpemVfY291bnRzX3dpZGUgPC0gc3RvcmVfc2l6ZV9jb3VudHMgJT4lDQogIHNlbGVjdCgtbikgJT4lDQogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSByZXRhaWxlciwgdmFsdWVzX2Zyb20gPSBwZXJjZW50YWdlX3N0b3JlcykgJT4lDQogIHJlcGxhY2UoaXMubmEoLiksIDApDQoNCnRhcmdldF9icmFuZHMgPC0gc3RvcmVfc2l6ZV9jb3VudHMgJT4lDQogIHNlbGVjdChyZXRhaWxlcikgJT4lDQogIHVuaXF1ZSgpDQoNCnRhcmdldF9icmFuZHMgPC0gdGFyZ2V0X2JyYW5kc1t0YXJnZXRfYnJhbmRzICE9ICdUZXNjbyddDQoNCmZvciAoYnJhbmQgaW4gdGFyZ2V0X2JyYW5kcyl7DQogIA0KICBjb21wYXJpc29ucyA8LSBzaXplX2NvdW50c193aWRlICU+JQ0KICAgIHNlbGVjdChUZXNjbywgYnJhbmQpDQogIA0KICBjaGlzcSA8LSBjaGlzcS50ZXN0KGNvbXBhcmlzb25zKQ0KICANCiAgcHJpbnQoYnJhbmQpDQogIHByaW50KGNoaXNxKQ0KfQ0KYGBgDQpGcm9tIHRoZSBhYm92ZSB3ZSB3YW50IHRvIGtlZXAgaW4gbWluZCB0aGUgbnVsbCBoeXBvdGhlc2lzIC0gd2hpY2ggaXMgdG8gc2F5IHRoYXQgYXMgcC12YWx1ZSB0ZW5kcyB0b3dhcmRzIDAsIHRoZSBpbnRlcmRlcGVuZGVuY2UgYmV0d2VlbiB0aGUgdHdvIGRpc3RyaWJ1dGlvbnMgKGkuZS4gc2ltaWxhcml0eSBpbiBiZWhhdmlvdXIpIGJlY29tZXMgbGVzcy4gVGhlb3JldGljYWxseSBpZiBwPj0wLjA1IHRoZW4gdGhlIHZhcmlhYmxlcyBhcmUgbm90IGludGVyZGVwZW5kZW50Lg0KDQpJbiBvdXIgY2FzZSAod2hpbCBub3QgPj0gMC4wNSkgd2Ugc2VlIHRoYXQgdGhlIHAtdmFsdWUgZm9yIE1vcnJpc29ucyBpcyBvcmRlcnMgb2YgbWFnbml0dWRlIGdyZWF0ZXIgdGhhbiBmb3IgdGhlIG90aGVyIGJyYW5kcywgbWVhbmluZyBpdCBoYXMgdGhlIG1vc3Qgc2ltaWxhciBwcm9maWxlIC0gYXMgZXhwZWN0ZWQuIA0KDQpJbiBmYWN0LCBBc2RhIGhhcyBhIGRpc3RpbmN0bHkgZGlmZmVyZW50IGJ1c2luZXNzIG1vZGVsLCB3aXRoIGZhciBncmVhdGVyIGZvY3VzICh3aXRoID41MCUgb2YgdGhlaXIgc3RvcmVzKSBvbiBsYXJnZSBzdXBlcnN0b3JlcyAob3ZlciAyODAwbTIpLiBNZWFud2hpbGUsIHRoZSBDby1vcGVyYXRpdmUgb3BlcmF0ZXMgYSBzZXQgb2YgcHJpbWFyaWx5IHNtYWxsIHN0b3Jlcywgd2l0aCB6ZXJvIHN1cGVyc3RvcmVzIGluIHRoZSBncm91cC4NCg0KR2l2ZW4gTW9ycmlzb25zIGhhcyBvbmUgb2YgdGhlIGxvd2VyICUgb2Ygb3ZlcmxhcCwgYW5kIHRoZSBtb3N0IHNpbWlsYXIgcHJvZmlsZSwgd2UgZWxlY3QgdG8gImFjcXVpcmUiIGl0DQoNCmBgYHtyfQ0KIyBjcmVhdGUgYSBjb21iaW5lZCBkYXRhc2V0DQpjb21iaW5lZF9zdG9yZXMgPC0gbWFqb3JfcmV0YWlsZXJzICU+JQ0KICBmaWx0ZXIocmV0YWlsZXIgJWluJSBjKCdUZXNjbycsICdNb3JyaXNvbnMnKSkNCg0KIyBub3cgd2UgaGF2ZSBwcmV0dHkgZ29vZCBjb3ZlcmFnZSBhY3Jvc3MgdGhlIFVLDQpwIDwtIGxlYWZsZXQoY29tYmluZWRfc3RvcmVzKSAlPiUgYWRkVGlsZXMoKSAlPiUNCiAgYWRkQ2lyY2xlcyh+bG5nLCB+bGF0LGNvbG9yID0gJ2JsdWUnLCByYWRpdXMgPSA1MDAsIG9wYWNpdHkgPSAuNSkgJT4lDQogIG92ZXJsYXlUaXRsZSgiVGVzY28gU3RvcmUgTG9jYXRpb25zIik7IHANCg0KIyBzYXZlIGRvd24gY29tYmluZWQgZGF0YXNldA0Kd3JpdGVfcmRzKGNvbWJpbmVkX3N0b3JlcywgImRhdGEvY29tYmluZWRfc3RvcmUucmRzIikNCmBgYA0KDQoNCg0KIyMjIElkZW50aWZ5aW5nIFdoaWNoIFN0b3JlcyB0byBDbG9zZQ0KDQpOb3cgd2UgaGF2ZSBhIGdyb3VwIG9mIHN0b3JlIGxvY2F0aW9ucywgbmV4dCB3ZSBjYW4gb3B0aW1pc2UgdGhlIHBvcnRmb2xpbyB3ZSB3aWxsIGRvIHRoaXMgYnkgb3B0aW5nIHRvIGNsb3NlIHN0b3JlcyBpbiB0aGUgc2FtZSBsb2NhdGlvbiBhbmQgdGhlbiBkZWZpbmluZyBzb21lIGxvZ2ljIHRvIHByaW9yaXRpc2Ugd2hpY2ggc3RvcmVzIHRvIGtlZXAgc3RvcmVzDQoNCg0KYGBge3J9DQojIGxvYWQgY2hlY2twb2ludCBkYXRhc2V0DQpjb21iaW5lZF9zdG9yZXMgPC0gcmVhZF9yZHMoImRhdGEvY29tYmluZWRfc3RvcmUucmRzIikNCg0KIyBzcGxpdCBieSBicmFuZA0KdGVzY29fc3RvcmVzIDwtIGNvbWJpbmVkX3N0b3JlcyAlPiUNCiAgZmlsdGVyKHJldGFpbGVyID09ICdUZXNjbycpDQoNCm1vcnJpc29uc19zdG9yZXMgPC0gY29tYmluZWRfc3RvcmVzICU+JQ0KICBmaWx0ZXIocmV0YWlsZXIgPT0gJ01vcnJpc29ucycpDQoNCg0KIyBjb252ZXJ0IGJvdGggdGFibGVzIHRvIHNmIHN0YW5kYXJkDQp0ZXNjb19zdG9yZXNfc2YgPC0gc3RfYXNfc2YodGVzY29fc3RvcmVzLCBjb29yZHM9YygnbG5nJywgJ2xhdCcpLCBjcnM9ImVwc2c6NDMyNiIpDQptb3JyaXNvbnNfc3RvcmVzX3NmIDwtIHN0X2FzX3NmKG1vcnJpc29uc19zdG9yZXMsIGNvb3Jkcz1jKCdsbmcnLCAnbGF0JyksIGNycz0iZXBzZzo0MzI2IikNCg0KIyBjb21wdXRlIGRpc3RhbmNlIG1hdHJpeA0KZGlzdF9zZiA9IHN0X2Rpc3RhbmNlKHRlc2NvX3N0b3Jlc19zZiwgbW9ycmlzb25zX3N0b3Jlc19zZikNCg0KIyBub3RlOiBlYWNoIHJvdyByZXByZXNlbnRzIGEgVGVzY28gbG9jYXRpb24gYW5kIGNvbHVtbnMgdGhlaXIgZGlzdGFuY2UgdG8gYSBNb3JyaXNvbnMgc3RvcmUgKGluIG1ldGVycykNCk0gPC0gYXMubWF0cml4KGRpc3Rfc2YpDQpNIDwtIHVuY2xhc3MoTSkNCg0KI2NyZWF0ZSBiaW5hcnkgbWF0cml4IHRvIHNob3cgd2hlcmUgVGVzY28gc3RvcmUgKHJvdykgd2l0aGluIDUwMG0gb2YgTW9ycmlzb25zIChjb2x1bW4pIA0KTVtdIDwtIGlmZWxzZShNPDUwMCwxLDApDQoNCiMgY29udmVydCB0byBkYXRhZnJhbWUgYW5kIGFkZCBjb2x1bW4gdG8gaW5kaWNhdGUgVGVzY28gaW5kZXggbnVtYmVyDQpkaXN0X3NmIDwtIGRhdGEuZnJhbWUoTSkNCg0KZGlzdF9zZiA8LSByb3duYW1lc190b19jb2x1bW4oZGlzdF9zZikgJT4lDQogIHJlbmFtZSh0ZXNjb19pbmRleCA9IHJvd25hbWUpDQoNCiMgY29udmVydCBmcm9tIHdpZGUtZm9ybSB0byBsb25nLWZvcm0NCmNsb3NlX3N0b3JlX2RmIDwtIGRpc3Rfc2YgJT4lIA0KICBnYXRoZXIoa2V5ID0gbW9ycmlzb25zX2luZGV4LCB2YWx1ZSA9IGZsYWcsIC1jKHRlc2NvX2luZGV4KSkgJT4lDQogIGZpbHRlcihmbGFnID09IDEpDQoNCiMgY2xlYW4gdXAgdGhlIG1vcnJpb25zIGluZGV4IGNvbHVtbiAtIHJlbW92aW5nIHRoZSAiWCIgZnJvbSB0aGUgbmFtaW5nIGNvbnZlbnRpb24NCmNsb3NlX3N0b3JlX2RmIDwtIGNsb3NlX3N0b3JlX2RmICU+JQ0KICByb3d3aXNlKCkgJT4lDQogIG11dGF0ZShtb3JyaXNvbnNfaW5kZXggPSBzdHJfcmVtb3ZlKG1vcnJpc29uc19pbmRleCwgIlgiKSkgJT4lDQogIHVuZ3JvdXANCg0KIyBjaGVjayBpZiB3ZSBoYXZlIGFueSBkdXBsaWNhdGVzIChpLmUuIG9uZSBzdG9yZSBjbG9zZSB0byB0d28gb3RoZXJzKQ0KbnJvdyhjbG9zZV9zdG9yZV9kZikNCiMgMTIxDQpuX2Rpc3RpbmN0KGNsb3NlX3N0b3JlX2RmJHRlc2NvX2luZGV4KQ0KIyAxMTcNCm5fZGlzdGluY3QoY2xvc2Vfc3RvcmVfZGYkbW9ycmlzb25zX2luZGV4KQ0KIyAxMTENCg0KIyBqb2luIGJhY2sgdG8gdGhlIFRlc2NvIG1hc3RlciB0byBpbmRpY2F0ZSBNb3JyaXNvbnMgbG9jYXRpb25zDQp0ZXNjb19zdG9yZXNfZGYgPC0gcm93bmFtZXNfdG9fY29sdW1uKHRlc2NvX3N0b3JlcykgJT4lDQogIHJlbmFtZSh0ZXNjb19pbmRleCA9IHJvd25hbWUpICU+JQ0KICBpbm5lcl9qb2luKGNsb3NlX3N0b3JlX2RmLCBieT0ndGVzY29faW5kZXgnKQ0KDQojIGpvaW4gb24gdGhlIHJlbGV2YW50IE1vcnJpc29ucyBzdG9yZSBkYXRhDQptb3JyaXNvbnNfc3RvcmVzX2RmIDwtIHJvd25hbWVzX3RvX2NvbHVtbihtb3JyaXNvbnNfc3RvcmVzKSAlPiUNCiAgcmVuYW1lKG1vcnJpc29uc19pbmRleCA9IHJvd25hbWUpDQogIA0Kc3RvcmVfcGFpcnMgPC0gdGVzY29fc3RvcmVzX2RmICU+JQ0KICBsZWZ0X2pvaW4obW9ycmlzb25zX3N0b3Jlc19kZiwgYnk9J21vcnJpc29uc19pbmRleCcpDQpgYGANCk5vdyB3ZSBoYXZlIGZvdW5kIGFsbCB0aGUgc3RvcmUgcGFpcnMgKGkuZS4gdGhvc2UgdGhhdCBhcmUgd2l0aGluIDUwMG0gb2YgZWFjaCBvdGhlcikgZnJvbSBhY3Jvc3MgdGhlIFRlc2NvIHZzIE1vcnJpc29uJ3MgcG9ydGZvbGlvLiBUaGUgbmV4dCBzdGFnZSB3b3VsZCBiZSB0byBkZWNpZGUgdGhlIGxvZ2ljIG9uIHdoaWNoIHRvIGtlZXAgc3RvcmVzLg0KDQpJbiBvdXIgY2FzZSAoYW5kIGZvciBlYXNlIG9mIHByb2dyZXNzaW5nIHdpdGggdGhlIHJlc3Qgb2YgdGhlIHByb2plY3Qgbm90ZWJvb2spLCB3ZSB3aWxsIGFzc3VtZSB0aGF0IHdlIHdpbGwgc2ltcGx5IHJlbW92ZSB0aGUgTW9ycmlzb24ncyBzdG9yZSwgYW5kIGtlZXAgdGhlIGV4aXN0aW5nIFRlc2NvIHN0b3Jlcy4NCg0KSG93ZXZlciwgaW4gcHJhY3RpY2UgeW91IG1heSB3aXNoIHRvIGxvb2sgYXQgZGVtb2dyYXBoaWMgKHNwZWNpZmljYWxseSBob3VzZWhvbGQgaW5jb21lLCBvciBwb3B1bGF0aW9uIGRlbnNpdHkpIGRhdGEgYnkgcG9zdGNvZGUsIGFuZCBtYWtlIHNvbWUgYXNzdW1wdGlvbnMgYXJvdW5kIHdoZXRoZXIgeW91IHdhbnQgaGlnaGVyIHByaWNlIGdvb2RzICh1c3VhbGx5IGluIHNtYWxsZXIgc3RvcmVzKSBpbiBhcmVhcyBvZiBoaWdoZXIgZGVuc2l0eSBvciBpbmNvbWUuIA0KDQpgYGB7cn0NCnJlbW92ZV9pZHMgPC0gc3RvcmVfcGFpcnMkaWQueSANCg0KY29tYmluZWRfY3V0IDwtIGNvbWJpbmVkX3N0b3JlcyAlPiUgDQogIGZpbHRlcighaWQgJWluJSByZW1vdmVfaWRzKQ0KDQpucmVtb3ZlID0gbnJvdyhjb21iaW5lZF9zdG9yZXMpIC0gbnJvdyhjb21iaW5lZF9jdXQpDQpwcmludChwYXN0ZTAoJ251bWJlciBvZiBzdG9yZXMgcmVtb3ZlZDogJywgbnJlbW92ZSkpDQoNCndyaXRlX3Jkcyhjb21iaW5lZF9jdXQsICJkYXRhL2NvbWJpbmVkX2N1dC5yZHMiKQ0KYGBgDQoNCg0KDQojIyMgSWRlbnRpZnlpbmcgTW9zdCBBcHByb3ByaWF0ZSBXYXJlaG91c2UgTG9jYXRpb25zDQoNCkFmdGVyIGNyZWF0aW5nIGEgY29tYmluZWQgcG9ydGZvbGlvICJwb3N0IGFjcXVpc2l0aW9uIiwgd2Ugd2lsbCBkZWNpZGUgd2hlcmUgYmVzdCB0byBzZXQgdXAgc3VwcGx5IGRlcG90cyB0byBzdG9jayB0aGUgc3RvcmVzLCB3aGljaCB3ZSB3aWxsIGFjaGlldmUgYnkgZmlyc3QgZ3JvdXBpbmcgc3RvcmVzIGludG8gc2Vuc2libGUgZ2VvZ3JhcGhpYyByZWdpb25zIC0gYW5kIHRoZW4gYXNzaWduIGEgZGVwb3QgdG8gZWFjaCByZWdpb24uIA0KDQpUaGUgbWV0aG9kIHRha2Ugd2lsbCBiZSB0byBmb3JtIGNsdXN0ZXJzLCBiYXNlZCBvbiBnZW9zcGF0aWFsIGxvY2F0aW9uIHRoYXQgY2FwdHVyZSB0aGUgY2xvc2VzdCBzdG9yZXMsIHdoaWxlIG1pbmltaXNpbmcgb3ZlcmxhcC4gVGhlIGRpc3RyaWJ1dGlvbiB3YXJlaG91c2VzL2RlcG90cyB3aWxsIHRoZW4gYmUgbG9jYXRlZCBhdCB0aGUgY2VudHJvaWRzIG9mIHRoZSBjbHVzdGVycy4NCg0KVGhlIGFpbSBpcyB0byBzcGxpdCB0aGUgbG9jYXRpb25zIGludG8gYy4yMCByZWdpb25zDQoNCmBgYHtyfQ0KIyBsb2FkIGNvbWJpbmVkIHN0b3JlIHNldA0KbWFzdGVyX3N0b3JlcyA8LSByZWFkX3JkcygiZGF0YS9jb21iaW5lZF9jdXQucmRzIikNCg0KIyB0YWtlIGp1c3QgdGhlIGxhdCBhbmQgbG5nIGNvb3JkaW5hdGVzDQpsb2NzX2RmIDwtIG1hc3Rlcl9zdG9yZXMgJT4lDQogIHNlbGVjdCgiaWQiLCJsbmciLCAibGF0IikgJT4lDQogIGNvbHVtbl90b19yb3duYW1lcygiaWQiKQ0KDQojIHNldCByYW5kb20gc2VlZCB0byBlbnN1cmUgcmVwZWF0YWJpbGl0eSBvZiBjbHVzdGVyaW5nDQpzZXQuc2VlZCgxMjMpDQpgYGANCg0KDQpBIGZldyBkaWZmZXJlbnQgY2x1c3RlcmluZyBhcHByb2FjaGVzIHdlcmUgdHJpYWxlZCBhcyBwYXJ0IG9mIHRoaXMgd29yazoNCi0gUGFydGl0aW9uLWJhc2VkIGNsdXN0ZXJpbmc6IEtNZWFucw0KLSBEZW5zaXR5LWJhc2VkIGNsdXN0ZXJpbmc6IERCU0NBTg0KLSBIaWVyYXJjaGljYWwgY2x1c3RlcmluZzogQWdnbG9tZXJhdGl2ZQ0KDQpXaGlsZSBLTWVhbnMgcHJvZHVjZWQgZ29vZCByZXN1bHRzLCB0aGlzIG1ldGhvaWQgd2FzIGRpc2NvdW50ZWQgYXMgaXQgaXMgYmFkIHByYWN0aWNlIHRvIHVzZSB3aXRoIGdlb3NwYXRpYWwgZGF0YS4gVGhpcyBpcyBiZWNhdXNlIGl0IGFzc3VtZXMgY29vcmRpbmF0ZXMgYXJlIGRlc2NyaWJlZCBpbiBFdWNsaWRlYW4gY29vcmRpbmF0ZSBzeXN0ZW0gKHdoaWNoIGxhdGl0dWRlIGFuZCBsb25naXR1ZGUgYXJlIG5vdCkuIFNvIHdoaWxlIGl0IG1heSBwcm9kdWNlIHJlYXNvbmFibGUgcmVzdWx0cyBmb3IgbG9jYXRpb25zIGNsb3NlIHRvZ2V0aGVyIG9uIEVhcnRoLCBpdCBmYWlscyB0byBhY2NvdW50IGZvciBhbnkgY3VydmF0dXJlIHdoZW4gY2x1c3RlcmluZyBsb2NhdGlvbnMgdGhhdCBhcmUgZmFyIGF3YXkNCg0KTWVhbndoaWxlLCBEQlNDQU4gKHdoaWNoIGxvb2tzIHRvIGNsdXN0ZXIgYmFzZWQgb24gdmFyaWF0aW9ucyBpbiBsb2NhdGlvbiBkZW5pc3R5KSBwcm9kdWNlZCBzdHJvbmcgY2x1c3RlcnMgZm9yIG1vc3QgdXJiYW4gbG9jYXRpb25zIChlLmcuIGJpZyBjaXRpZXMpLCBidXQgZWZmZWN0aXZlIGdyb3VwZWQgYWxsIGxlc3MgZGVuc2UgYXJlYXMgaW50byBhIGZldyBsYXJnZSBjbHVzdGVycyAod2hpY2ggaXMgaW5lZmZlY3RpdmUgZm9yIG91ciB1c2UgY2FzZSkNCg0KSGllcmFyY2hpY2FsIGNsdXN0ZXJpbmcgcHJvZHVjZWQgdGhlIGJlc3QgcmVzdWx0cyAtIHBsZWFzZSBza2lwIHRvIGJlbG93IGNvZGUgc2VnbWVudCB0byBzZWUNCg0KTm90ZTogV2hpbGUgY29kZSBzZWdtZW50cyBmb3IgYm90aCBLTWVhbnMgYW5kIERCU0NBTiBhcmUgcHJlc2VudGVkIGJlbG93LCBmb3IgaW50ZXJlc3Qgb25seS4gVG8gY29udGludWUgd2l0aCB0aGUgbm90ZWJvb2ssIHRoZXNlIGNhbiBiZSBza2lwcGVkLCBhbmQgeW91IGNhbiBqdW1wIHRvIHRoZSBIaWVyYXJjaGljYWwgY29kZSBiZWxvdw0KDQoNCktNRUFOUyAtIEZPUiBJTlRFUkVTVCBPTkxZDQpgYGB7cn0NCiMgQ29tcHV0ZSBrLW1lYW5zIHdpdGggayA9IDIwIChpLmUuIDIwIHJlZ2lvbnMpDQprbS5yZXMgPC0ga21lYW5zKGxvY3NfZGYsIDIwKQ0KDQojIGFzc2lnbiB0aGUgY2x1c3RlciBudW1iZXJzDQpsb2NzX2RmX2ttPC0gbG9jc19kZiAlPiUgDQogIG11dGF0ZShjbHVzdCA9IGttLnJlcyRjbHVzdGVyKQ0KDQojIHByaW50DQpsZW5ndGgodW5pcXVlKGxvY3NfZGZfa20kY2x1c3QpKQ0KIyAyMA0KDQojIHBsb3Qgb3V0cHV0cw0KcGFsIDwtIGNvbG9yRmFjdG9yKA0KICBwYWxldHRlID0gIlJkWWxCdSIsDQogIGRvbWFpbiA9IGxvY3NfZGZfa20kY2x1c3QpDQoNCnAgPC0gbGVhZmxldChsb2NzX2RmX2ttKSAlPiUgYWRkUHJvdmlkZXJUaWxlcyhwcm92aWRlcnMkQ2FydG9EQi5Qb3NpdHJvbikgJT4lDQogIGFkZENpcmNsZXMofmxuZywgfmxhdCxjb2xvciA9IH5wYWwoY2x1c3QpKSAlPiUNCiAgb3ZlcmxheVRpdGxlKCJTdG9yZSBDbHVzdGVycyIpICU+JQ0KICBhZGRMZWdlbmQocG9zaXRpb24gPSAiYm90dG9tcmlnaHQiLCB2YWx1ZXMgPSB+Y2x1c3QsIHBhbCA9IHBhbCk7IHANCg0KDQojIHBsb3Qgc3RvcmUgY291bnQgYnkgY2x1c3Rlcg0KY2x1c3Rlcl9jb3VudF9rbSA8LSBsb2NzX2RmX2ttICU+JQ0KICBncm91cF9ieShjbHVzdCkgJT4lDQogIHRhbGx5KCkNCg0KZ2dwbG90KGNsdXN0ZXJfY291bnRfa20sIGFlcyh4ID0gcmVvcmRlcihjbHVzdCwgLW4pLCB5ID0gbikpICsNCiAgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsgdGhlbWVfbWluaW1hbCgpICsNCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSAwLCBoanVzdCA9IDAuNSwgdmp1c3QgPSAwLjMpKQ0KDQpgYGANCg0KREJTQ0FOIC0gRk9SIElOVEVSRVNUIE9OTFkNCmBgYHtyfQ0KY2x1c3RlcnMgPC0gZGJzY2FuKGxvY3NfZGYsIGVwcyA9IDAuMjUsIG1pblB0cyA9IDcwKVtbJ2NsdXN0ZXInXV0NCg0KbGVuZ3RoKHVuaXF1ZShjbHVzdGVycykpDQojIDIwDQoNCmxvY3NfZGZfZGIgPC0gbG9jc19kZiAlPiUNCiAgbXV0YXRlKGNsdXN0ID0gY2x1c3RlcnMpDQoNCiMgcGxvdCBvdXRwdXRzDQpwYWwgPC0gY29sb3JGYWN0b3IoDQogIHBhbGV0dGUgPSAiUmRZbEJ1IiwNCiAgZG9tYWluID0gbG9jc19kZl9kYiRjbHVzdCkNCg0KcCA8LSBsZWFmbGV0KGxvY3NfZGZfZGIpICU+JSBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLlBvc2l0cm9uKSAlPiUNCiAgYWRkQ2lyY2xlcyh+bG5nLCB+bGF0LGNvbG9yID0gfnBhbChjbHVzdCkpICU+JQ0KICBvdmVybGF5VGl0bGUoIlN0b3JlIENsdXN0ZXJzIikgJT4lDQogIGFkZExlZ2VuZChwb3NpdGlvbiA9ICJib3R0b21yaWdodCIsIHZhbHVlcyA9IH5jbHVzdCwgcGFsID0gcGFsKTsgcA0KDQpgYGANCg0KDQoNCkhJRVJBUkNISUNBTCAtIENIT1NFTiBBUFBST0FDSA0KYGBge3J9DQojIGNvbXB1dGUgZGlzdGFuY2UgbWF0cml4IC0gYmV0d2VlbiBsb2NhdGlvbiBwYWlycw0KbG9jc19zZiA8LSBzdF9hc19zZihsb2NzX2RmLCBjb29yZHM9YygnbG5nJywgJ2xhdCcpLCBjcnM9ImVwc2c6NDMyNiIpDQpkaXN0X3NmID0gc3RfZGlzdGFuY2UobG9jc19zZiwgbG9jc19zZikNCm1kaXN0IDwtIGFzLm1hdHJpeChkaXN0X3NmKQ0KbWRpc3QgPC0gdW5jbGFzcyhtZGlzdCkNCg0KIyBjbHVzdGVyIGJhc2VkIG9uIGRpc3RhbmNlIHRvIG90aGVyIGxvY2F0aW9ucw0KaGMgPC0gaGNsdXN0KGFzLmRpc3QobWRpc3QpLCBtZXRob2Q9ImNvbXBsZXRlIikNCg0KIyBwbG90IGRlbmRvZ3JhbQ0KcGxvdChoYywgY2V4ID0gMC42LCBoYW5nID0gLTEpDQoNCiMgY2x1c3RlciBiYXNlZCBvbiBkZWZpbmVkIGRpc3RhbmNlIHNlcGFyYXRpb24gLSB0cmlhbGVkIGluIG9yZGVyIHRvIGdldCAyMCBjbHVzdGVycw0KZCA8LSAyMTUwMDANCmxvY3NfZGZfaGMgPC0gbG9jc19kZiAlPiUNCiAgbXV0YXRlKGNsdXN0ID0gY3V0cmVlKGhjLCBoPWQpKQ0KDQojIHByaW50DQpsZW5ndGgodW5pcXVlKGxvY3NfZGZfaGMkY2x1c3QpKQ0KIyAyMA0KDQojIHBsb3Qgb3V0cHV0cw0KcGFsIDwtIGNvbG9yRmFjdG9yKA0KICBwYWxldHRlID0gIlJkWWxCdSIsDQogIGRvbWFpbiA9IGxvY3NfZGZfaGMkY2x1c3QpDQoNCnAgPC0gbGVhZmxldChsb2NzX2RmX2hjKSAlPiUgYWRkUHJvdmlkZXJUaWxlcyhwcm92aWRlcnMkQ2FydG9EQi5Qb3NpdHJvbikgICU+JQ0KICBhZGRDaXJjbGVzKH5sbmcsIH5sYXQsY29sb3IgPSB+cGFsKGNsdXN0KSkgJT4lDQogIG92ZXJsYXlUaXRsZSgiU3RvcmUgQ2x1c3RlcnMiKSAlPiUNCiAgYWRkTGVnZW5kKHBvc2l0aW9uID0gImJvdHRvbXJpZ2h0IiwgdmFsdWVzID0gfmNsdXN0LCBwYWwgPSBwYWwpOyBwDQpgYGANCg0KV2hlbiB2aWV3aW5nIG9uIGEgbWFwLCBpdCBhcHBlYXJzIHRvIGJlIGFuIGluaXRpYWxseSB2ZXJ5IHNlbnNpYmxlIGNsdXN0ZXJpbmcuIEhvd2V2ZXIsIHRoZXJlIGxvb2tzIHRvIGJlIHNvbWUgdmVyeSBzbWFsbCBjbHVzdGVycywgd2hpY2ggaXQgbWF5IG5vdCBiZSBsb2dpY2FsIHRvIHRyZWF0IGFzIGEgY2x1c3RlciBpbiBhbmQgb2YgaXRzZWxmLg0KDQpXZSBleHBsb3JlIHRoaXMgZnVydGhlciBieSBsb29raW5nIGF0IHRoZSBzcHJlYWQgb2Ygc3RvcmUgY291bnRzIGluIGVhY2gNCg0KYGBge3J9DQoNCiMgcGxvdCBzdG9yZSBjb3VudCBieSBjbHVzdGVyDQpjbHVzdGVyX2NvdW50X2hjIDwtIGxvY3NfZGZfaGMgJT4lDQogIGdyb3VwX2J5KGNsdXN0KSAlPiUNCiAgdGFsbHkoKQ0KDQpnZ3Bsb3QoY2x1c3Rlcl9jb3VudF9oYywgYWVzKHggPSByZW9yZGVyKGNsdXN0LCAtbiksIHkgPSBuKSkgKw0KICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IikgKyB0aGVtZV9taW5pbWFsKCkgKw0KICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDAsIGhqdXN0ID0gMC41LCB2anVzdCA9IDAuMykpDQoNCiMgcGxvdCBzdG9yZSBjb3VudCBieSBjbHVzdGVyIGFzICUNCmNsdXN0ZXJfY291bnRfaGMgPC0gY2x1c3Rlcl9jb3VudF9oYyAlPiUNCiAgbXV0YXRlKHBlcmNlbnRhZ2UgPSAobiAvIG5yb3cobG9jc19kZl9oYykpKjEwMCkNCg0KZ2dwbG90KGNsdXN0ZXJfY291bnRfaGMsIGFlcyh4ID0gcmVvcmRlcihjbHVzdCwgLXBlcmNlbnRhZ2UpLCB5ID0gcGVyY2VudGFnZSkpICsNCiAgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsgdGhlbWVfbWluaW1hbCgpICsNCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSAwLCBoanVzdCA9IDAuNSwgdmp1c3QgPSAwLjMpKQ0KYGBgDQoNCkZyb20gdGhlIGFib3ZlIGNoYXJ0LCBpdCdzIG9idmlvdXMgdGhhdCB0aGVyZSBhcmUgY2x1c3RlcnMgd2l0aCBmZXcgc3RvcmUgbG9jYXRpb25zLiBQbG90dGluZyB0aGVzZSBvbiB0aGUgbWFwIChlLmcuIGJlbG93IGNvZGUgc2VnbWVudCksIHNob3dzIHRoYXQgdGhlc2UgdGVuZCB0byBiZSBpbiBpc29sYXRlZCBsb2NhdGlvbnMgLSBlLmcuIFNoZXRsYW5kIElzbGFuZHMsIEhlYnJpZGVzLCBPcmtuZXkgSXNsYW5kcywgb3IgR3Vlcm5zZXkgJiBKZXJzZXkNCg0KQWxsIG9mIHRoZXNlIGhhdmUgPDElIG9mIHRoZSB0b3RhbCBzdG9yZSBsb2NhdGlvbnMgKHdoaWNoIGlzIGhvdyB3ZSB3aWxsIGRlY2lkZSB0byByZW1vdmUgdGhlbSkNCmBgYHtyfQ0KIyBwbG90IG91dHB1dHMgLSBleHBsb3JhdG9yeSBvbmx5DQpjbHVzdGVyX251bWJlciA8LSAxOA0KDQpzYW1wbGVfY2x1c3RlciA8LSBsb2NzX2RmX2hjICU+JQ0KICBmaWx0ZXIoY2x1c3QgPT0gY2x1c3Rlcl9udW1iZXIpDQoNCnAgPC0gbGVhZmxldChzYW1wbGVfY2x1c3RlcikgJT4lIGFkZFByb3ZpZGVyVGlsZXMocHJvdmlkZXJzJENhcnRvREIuUG9zaXRyb24pICAlPiUNCiAgYWRkQ2lyY2xlcyh+bG5nLCB+bGF0LGNvbG9yID0gfnBhbChjbHVzdCkpICU+JQ0KICBvdmVybGF5VGl0bGUoIlN0b3JlIENsdXN0ZXJzIikgJT4lDQogIGFkZExlZ2VuZChwb3NpdGlvbiA9ICJib3R0b21yaWdodCIsIHZhbHVlcyA9IH5jbHVzdCwgcGFsID0gcGFsKTsgcA0KYGBgDQoNCg0KV2Ugd2lsbCByZW1vdmUgc3VjaCByZW1vdGUgbG9jYXRpb25zIGZyb20gb3VyIGFuYWx5c2lzIGFuZCByZS1jbHVzdGVyDQpgYGB7cn0NCiMgcmVtb3ZlIHRob3NlIHdpdGggbGVzcyB0aGFuIDElDQpyZW1vdGVfY2x1c3RlcnMgPC0gY2x1c3Rlcl9jb3VudF9oYyAlPiUNCiAgZmlsdGVyKHBlcmNlbnRhZ2UgPCAxKSAlPiUNCiAgc2VsZWN0KGNsdXN0KQ0KDQpsb2NzX2RmX2hjX2N1dCA8LSBsb2NzX2RmX2hjICU+JQ0KICBmaWx0ZXIoIWNsdXN0ICVpbiUgcmVtb3RlX2NsdXN0ZXJzJGNsdXN0KQ0KDQojIGNvbXB1dGUgZGlzdGFuY2UgbWF0cml4IC0gYmV0d2VlbiBsb2NhdGlvbiBwYWlycw0KbG9jc19zZiA8LSBzdF9hc19zZihsb2NzX2RmX2hjX2N1dCAlPiUgc2VsZWN0KGxuZywgbGF0KSwgY29vcmRzPWMoJ2xuZycsICdsYXQnKSwgY3JzPSJlcHNnOjQzMjYiKQ0KZGlzdF9zZiA9IHN0X2Rpc3RhbmNlKGxvY3Nfc2YsIGxvY3Nfc2YpDQptZGlzdCA8LSBhcy5tYXRyaXgoZGlzdF9zZikNCm1kaXN0IDwtIHVuY2xhc3MobWRpc3QpDQoNCiMgcGxvdCBkZW5kb2dyYW0NCmhjIDwtIGhjbHVzdChhcy5kaXN0KG1kaXN0KSwgbWV0aG9kPSJjb21wbGV0ZSIpDQpwbG90KGhjLCBjZXggPSAwLjYsIGhhbmcgPSAtMSkNCg0KIyBjbHVzdGVyIGJhc2VkIG9uIGRlZmluZWQgZGlzdGFuY2Ugc2VwYXJhdGlvbiAtIHRyaWFsIGFuZCBlcnJvIHRvIGdldCByZWFzb25hYmxlIGNsdXN0ZXJzDQpkIDwtIDE3NTAwMA0KbG9jc19kZl9oY19jdXQgPC0gbG9jc19kZl9oY19jdXQgJT4lDQogIG11dGF0ZShjbHVzdDIgPSBjdXRyZWUoaGMsIGg9ZCkpDQoNCiMgcHJpbnQNCmxlbmd0aCh1bmlxdWUobG9jc19kZl9oY19jdXQkY2x1c3QyKSkNCiMgMjANCg0KDQojIHBsb3Qgb3V0cHV0cw0KcGFsIDwtIGNvbG9yRmFjdG9yKA0KICBwYWxldHRlID0gIlJkWWxCdSIsDQogIGRvbWFpbiA9IGxvY3NfZGZfaGNfY3V0JGNsdXN0MikNCg0KcCA8LSBsZWFmbGV0KGxvY3NfZGZfaGNfY3V0KSAlPiUgYWRkUHJvdmlkZXJUaWxlcyhwcm92aWRlcnMkQ2FydG9EQi5Qb3NpdHJvbikgICU+JQ0KICBhZGRDaXJjbGVzKH5sbmcsIH5sYXQsY29sb3IgPSB+cGFsKGNsdXN0MikpICU+JQ0KICBvdmVybGF5VGl0bGUoIlN0b3JlIENsdXN0ZXJzIikgJT4lDQogIGFkZExlZ2VuZChwb3NpdGlvbiA9ICJib3R0b21yaWdodCIsIHZhbHVlcyA9IH5jbHVzdDIsIHBhbCA9IHBhbCk7IHANCmBgYA0KDQpOb3cgdGhhdCB3ZSBoYXZlIHNlbnNpYmxlIGNsdXN0ZXJzIGZvcm1pbmcsIHRoZSBuZXh0IHN0ZXAgaXMgdG8gZGV0ZXJtaW5lIHRoZSBsb2NhdGlvbiBvZiB0aGUgd2FyZWhvdXNlcy9kZXBvdHMgdGhhdCB3aWxsIHNlcnZpY2UgZWFjaCByZWdpb24uIEZvciB0aGlzLCBJIHdpbGwgdXNlIHNpbXBseSB0aGUgY2VudHJvaWRzIC0gd2hpY2ggY2FuIGJlIGNvbXB1dGVkIHVzaW5nIHRoZSBtZWFuIGxhdCxsbmcgdmFsdWVzDQoNCmBgYHtyfQ0KIyBjb21wdXRlIHRoZSBjZW50cmUgb2YgZWFjaCBjbHVzdGVyIGFuZCBwbG90IHRoaXMNCmNsdXN0ZXJfY2VudHJvaWRzIDwtIGxvY3NfZGZfaGNfY3V0ICU+JQ0KICBncm91cF9ieShjbHVzdDIpICU+JQ0KICBzdW1tYXJpemUobWVhbl9sbmcgPSBtZWFuKGxuZywgbmEucm09VFJVRSksIG1lYW5fbGF0ID0gbWVhbihsYXQsIG5hLnJtPVRSVUUpKQ0KDQoNCnAgPC0gbGVhZmxldChsb2NzX2RmX2hjX2N1dCkgJT4lIGFkZFByb3ZpZGVyVGlsZXMocHJvdmlkZXJzJENhcnRvREIuUG9zaXRyb24pICAlPiUNCiAgYWRkQ2lyY2xlcyh+bG5nLCB+bGF0LGNvbG9yID0gfnBhbChjbHVzdDIpKSAlPiUNCiAgb3ZlcmxheVRpdGxlKCJTdG9yZSBDbHVzdGVycyIpICU+JQ0KICBhZGRMZWdlbmQocG9zaXRpb24gPSAiYm90dG9tcmlnaHQiLCB2YWx1ZXMgPSB+Y2x1c3QyLCBwYWwgPSBwYWwpICU+JQ0KICBhZGRNYXJrZXJzKGRhdGEgPSBjbHVzdGVyX2NlbnRyb2lkcywgfm1lYW5fbG5nLCB+bWVhbl9sYXQsIGxhYmVsID0gfmFzLmNoYXJhY3RlcihjbHVzdDIpKTsgcA0KYGBgDQoNCk5vdyB3ZSBoYXZlIHRoZSB3YXJlaG91c2UgbG9jYXRpb25zLCBhbmQgc29tZSBpbml0aWFsIGNsdXN0ZXJzLiBIb3dldmVyLCB3ZSB3aWxsIHNlZSB0aGF0IGluIHNvbWUgY2FzZXMgdGhlIGhpZXJhcmNoaWNhbCBjbHVzdGVyaW5nIGRvZXMgbm90IHJlc3VsdCBpbiBzZW5zaWJsZSBhc3NpZ25tZW50cy4gRm9yIGV4YW1wbGUsIHRoZXJlIGFyZSBzaXR1YXRpb25zIHdoZXJlIGJvZGllcyBvZiB3YXRlciBleGlzdCB0aGF0IHNpbXBseSBjbHVzdGVyaW5nIGJ5IGNvb3JkaW5hdGVzIChsYXQsIGxuZykgd2lsbCBiZSB1bmFibGUgdG8gdGFrZSBhY2NvdW50IG9mLg0KDQplLmcuIGluIHRoZSBiZWxvdywgd2UgY2FuIHNlZSB0aGF0IGEgbG9jYXRpb24gaXMgb24gdGhlIG90aGVyIHNpZGUgb2YgdGhlIGVzdHVhcnkNCmBgYHtyfQ0KY2x1c3Rlcl9udW1iZXIgPC0gOA0KDQpzYW1wbGVfY2x1c3RlciA8LSBsb2NzX2RmX2hjX2N1dCAlPiUNCiAgZmlsdGVyKGNsdXN0MiA9PSBjbHVzdGVyX251bWJlcikNCg0Kc2FtcGxlX2NlbnRyb2lkIDwtIGNsdXN0ZXJfY2VudHJvaWRzICU+JQ0KICBmaWx0ZXIoY2x1c3QyID09IGNsdXN0ZXJfbnVtYmVyKQ0KDQpwIDwtIGxlYWZsZXQoc2FtcGxlX2NsdXN0ZXIpICU+JSBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLlBvc2l0cm9uKSAgJT4lDQogIGFkZENpcmNsZXMofmxuZywgfmxhdCxjb2xvciA9IH5wYWwoY2x1c3QpKSAlPiUNCiAgb3ZlcmxheVRpdGxlKCJTdG9yZSBDbHVzdGVycyIpICU+JQ0KICBhZGRMZWdlbmQocG9zaXRpb24gPSAiYm90dG9tcmlnaHQiLCB2YWx1ZXMgPSB+Y2x1c3QyLCBwYWwgPSBwYWwpICU+JQ0KICBhZGRNYXJrZXJzKGRhdGEgPSBzYW1wbGVfY2VudHJvaWQsIH5tZWFuX2xuZywgfm1lYW5fbGF0KTsgcA0KYGBgDQoNCkluIHByYWN0aWNlIHRoaXMgbWVhbnMgdGhhdCB0aGUgZHJpdmUgZGlzdGFuY2UgcmVxdWlyZWQgZnJvbSB0aGUgZGVwb3QgdG8gcmVzdG9jayB0aGUgc3RvcmUgaXMgZmFyIGdyZWF0ZXIgdGhhbiBpbml0aWFsbHkgYW50aWNpcGF0ZWQuIEZ1cnRoZXJtb3JlLCB0aGVyZSBtYXkgYmUgYSBuZWFyZXIgZGVwb3QgKGJlbG9uZ2luZyB0byBhbm90aGVyIGNsdXN0ZXIpIHRoYXQgaXMgYmV0dGVyIHN1aXRlZCBmb3IgcmVzdG9ja2luZyB0aGlzIHN0b3JlLg0KDQpGb3IgdGhpcyB3ZSB3aWxsIG5lZWQgdG8gdW5kZXJzdGFuZCB0aGUgZHJpdmUgdGltZSBhbmQgcmVhc3NpZ24gbG9jYXRpb25zIHRvIHRoZWlyIG5lYXJlc3QgY2x1c3RlciBjZW50cmUgYmFzZWQgb24gdGhpcy4NCg0KYGBge3J9DQojIGNyZWF0ZSBhIGNoZWNrcG9pbnQNCmxvY3NfZGZfaGNfY3V0IDwtIGxvY3NfZGZfaGNfY3V0ICU+JQ0KICBzZWxlY3QoLWNsdXN0KSAlPiUNCiAgcmVuYW1lKGNsdXN0ZXIgPSBjbHVzdDIpDQoNCmNsdXN0ZXJfY2VudHJvaWRzIDwtIGNsdXN0ZXJfY2VudHJvaWRzICU+JQ0KICByZW5hbWUobGF0PW1lYW5fbGF0LCBsbmc9bWVhbl9sbmcsIGNsdXN0ZXIgPSBjbHVzdDIpDQoNCndyaXRlX3Jkcyhsb2NzX2RmX2hjX2N1dCwgImRhdGEvY3V0X3N0b3JlX2xvY3MucmRzIikNCndyaXRlX3JkcyhjbHVzdGVyX2NlbnRyb2lkcywgImRhdGEvd2FyZWhvdXNlX2xvY2F0aW9ucy5yZHMiKQ0KYGBgDQoNCg0KDQoNCiMjIyBPcHRpbWlzZSBCYXNlZCBvbiBEcml2ZSBUaW1lDQoNCkZvciB0aGlzIHBhcnQsIHdlIHdpbGwgbGV2ZXJhZ2UgdGhlIGJyaWxsaWFudCBQcm9qZWN0IE9TUk0gKE9wZW4gU291cmNlIFJvdXRpbmcgTWFjaGluZSksIHdoaWNoIGNhbiBiZSBmb3VuZCBoZXJlOiBodHRwczovL2dpdGh1Yi5jb20vUHJvamVjdC1PU1JNL29zcm0tYmFja2VuZA0KDQpJdCB3aWxsIGxldmVyYWdlIGRvY2tlciBhbmQgYWxsb3cgdXMgdG8gcnVuIGEgbG9jYWwgYXBwLCB3aGljaCB3ZSBjYW4gc2VuZCBBUEkgcmVxdWVzdCB0byBpbiBvcmRlciBkZXRlcm1pbmUgdGhlIGRyaXZlIGRpc3RhbmNlIGFuZCB0aW1lIGJldHdlZW4gbG9jYXRpb24gcGFpcnMuDQoNCkZvciBmdXJ0aGVyIGRldGFpbHMgb24gaW5zdGFsbGF0aW9uIGFuZCBydW5uaW5nIHNlZSB0aGUgImluc3RhbGxfb3NybV9kb2NrZXIuUm1kIiBmaWxlLg0KDQpgYGB7cn0NCnN0b3JlX2xvY2F0aW9ucyA8LSByZWFkX3JkcygiZGF0YS9jdXRfc3RvcmVfbG9jcy5yZHMiKQ0Kd2FyZWhvdXNlX2xvY2F0aW9ucyA8LSByZWFkX3JkcygiZGF0YS93YXJlaG91c2VfbG9jYXRpb25zLnJkcyIpDQoNCiMgY3JlYXRlIHRoZSBzdHJpbmcgb2Ygd2FyZWhvdXNlIGxvY2F0aW9ucw0KY29vcmRzIDwtIGMoKQ0KZm9yIChyb3cgaW4gMTpucm93KHdhcmVob3VzZV9sb2NhdGlvbnMpKXsNCiAgd2FyZWhvdXNlIDwtIHBhc3RlKHdhcmVob3VzZV9sb2NhdGlvbnMkbG5nW3Jvd10sIHdhcmVob3VzZV9sb2NhdGlvbnMkbGF0W3Jvd10sIHNlcD0iLCIpDQogIGNvb3JkcyA8LSBhcHBlbmQoY29vcmRzLCB3YXJlaG91c2UpDQogIA0KfQ0Kd2FyZWhvdXNlX3N0cmluZyA8LSBwYXN0ZShjb29yZHMsIGNvbGxhcHNlPSI7IikNCg0KIyBpdGVyYXRlIHRocm91Z2ggZWFjaCBzdG9yZSBhbmQgZmluZCB0aGUgbmVhcmVzdCB3YXJlaG91c2UvZGVwb3QNCmNsdXN0ZXJfdmVjdG9yIDwtIGMoKQ0KZm9yIChyb3cgaW4gMTpucm93KHN0b3JlX2xvY2F0aW9ucykpew0KICBsb2NhdGlvbiA8LSBzdG9yZV9sb2NhdGlvbnMgJT4lDQogICAgc2VsZWN0KGxuZywgbGF0KSAlPiUNCiAgICBzbGljZShyb3cpICU+JQ0KICAgIHBhc3RlKGNvbGxhcHNlPSIsIikNCiAgDQogIHJlc3BvbnNlIDwtIEdFVChwYXN0ZTAoImh0dHA6Ly8xMjcuMC4wLjE6NTAwMC90YWJsZS92MS9kcml2aW5nLyIsIGxvY2F0aW9uLCAiOyIsIHdhcmVob3VzZV9zdHJpbmcsICI/c291cmNlcz0wIikpDQogIHJlc3VsdCA8LSBjb250ZW50KHJlc3BvbnNlLCBhcz0ncGFyc2VkJykNCiAgDQogIGR1cmF0aW9uX21hdHJpeCA8LSByZXN1bHQkZHVyYXRpb25zW1sxXV1bLTFdIA0KICANCiAgbmVhcmVzdF93YXJlaG91c2UgPC0gd2FyZWhvdXNlX2xvY2F0aW9ucyAlPiUNCiAgICBtdXRhdGUoc3RvcmVfdHJhdmVsX2R1cmF0aW9uID0gYXMubnVtZXJpYyhkdXJhdGlvbl9tYXRyaXgpKSAlPiUNCiAgICBzbGljZSh3aGljaC5taW4oLiRzdG9yZV90cmF2ZWxfZHVyYXRpb24pKSAlPiUNCiAgICBwdWxsKGNsdXN0ZXIpDQogIA0KICBjbHVzdGVyX3ZlY3RvciA8LSBhcHBlbmQoY2x1c3Rlcl92ZWN0b3IsIG5lYXJlc3Rfd2FyZWhvdXNlKQ0KICANCiAgaWYgKHJvdyUlMTAgPT0gMCl7DQogICAgcHJpbnQocGFzdGUwKHJvdywiLyIsbnJvdyhzdG9yZV9sb2NhdGlvbnMpKSkNCiAgfQ0KICANCn0NCg0Kc3RvcmVfbG9jYXRpb25zX3VwZGF0ZWQgPC0gc3RvcmVfbG9jYXRpb25zICU+JQ0KICBtdXRhdGUodXBkYXRlZF9jbHVzdGVyID0gY2x1c3Rlcl92ZWN0b3IpICU+JQ0KICByZW5hbWUob3JpZ2luYWxfY2x1c3RlciA9IGNsdXN0ZXIpICU+JQ0KICBtdXRhdGUoc2FtZV9jbHVzdGVyID0gY2FzZV93aGVuKA0KICAgIG9yaWdpbmFsX2NsdXN0ZXIgPT0gdXBkYXRlZF9jbHVzdGVyIH4gMSwNCiAgICBvcmlnaW5hbF9jbHVzdGVyICE9IHVwZGF0ZWRfY2x1c3RlciB+IDANCiAgICApKQ0KDQoNCndyaXRlX3JkcyhzdG9yZV9sb2NhdGlvbnNfdXBkYXRlZCwgImRhdGEvbWFzdGVyX3N0b3JlX2RmLnJkcyIpDQpgYGANCg0KTm93IHdlIGNhbiByZS1wbG90IHRoZSB1cGRhdGVkIGNsdXN0ZXIgYXNzaWdubWVudHMgdG8gY2hlY2sgdGhhdCBwcmV2aW91cyBpc3N1ZXMgaGF2ZSBiZWVuIGFkZHJlc3NlZA0KDQpgYGB7cn0NCnN0b3JlX2xvY2F0aW9uc191cGRhdGVkIDwtIHJlYWRfcmRzKCJkYXRhL21hc3Rlcl9zdG9yZV9kZi5yZHMiKQ0Kd2FyZWhvdXNlX2xvY2F0aW9ucyA8LSByZWFkX3JkcygiZGF0YS93YXJlaG91c2VfbG9jYXRpb25zLnJkcyIpDQoNCg0KIyBwbG90IHRoZSBuZXcgY2x1c3RlcnMNCnBhbCA8LSBjb2xvckZhY3RvcigNCiAgcGFsZXR0ZSA9ICJSZFlsQnUiLA0KICBkb21haW4gPSBzdG9yZV9sb2NhdGlvbnNfdXBkYXRlZCR1cGRhdGVkX2NsdXN0ZXIpDQoNCnAgPC0gbGVhZmxldChzdG9yZV9sb2NhdGlvbnNfdXBkYXRlZCkgJT4lIGFkZFByb3ZpZGVyVGlsZXMocHJvdmlkZXJzJENhcnRvREIuUG9zaXRyb24pICAlPiUNCiAgYWRkQ2lyY2xlcyh+bG5nLCB+bGF0LGNvbG9yID0gfnBhbCh1cGRhdGVkX2NsdXN0ZXIpLCBsYWJlbCA9IH5hcy5jaGFyYWN0ZXIodXBkYXRlZF9jbHVzdGVyKSkgJT4lDQogIG92ZXJsYXlUaXRsZSgiU3RvcmUgQ2x1c3RlcnMiKSAlPiUNCiAgYWRkTGVnZW5kKHBvc2l0aW9uID0gImJvdHRvbXJpZ2h0IiwgdmFsdWVzID0gfnVwZGF0ZWRfY2x1c3RlciwgcGFsID0gcGFsKSAlPiUNCiAgYWRkTWFya2VycyhkYXRhID0gd2FyZWhvdXNlX2xvY2F0aW9ucywgfmxuZywgfmxhdCwgbGFiZWwgPSB+YXMuY2hhcmFjdGVyKGNsdXN0ZXIpKTsgcA0KDQpgYGANCg0KDQoNCiMjIyBBc3NpZ25pbmcgTWFzdGVyIHZzIFN1YiBXYXJlaG91c2VzL0RlcG90cw0KDQpUaGUgbGFzdCB0aGluZyB3ZSB3aWxsIGRvIGlzIHRvIGRlY2lkZSB0aGUgYXBwcm9wcmlhdGUgbWl4IG9mIG1haW4gd2FyZWhvdXNlcy9kZXBvdHMsIGFuZCBzYXRlbGxpdGVzL3N1YnMuIFdlIHdpbGwgYWltIGZvciBqdXN0IDUgbWFzdGVyIGRlcG90cywgd2l0aCAxNSBzdWIgZGVwb3RzIGZvciBzZXJ2aWNpbmcgcmVnaW9ucy4NCg0KRm9yIHRoaXMgd2Ugd2lsbCBsb29rIGF0IHRoZSBzdG9yZSBjb3VudCBhc3NpZ25lZCB0byBlYWNoIGNsdXN0ZXIgKHdpdGggdGhlIGlkZWEgYmVpbmcgdG8gYXNzaWduIG1haW4gZGVwb3RzIGFzIHRob3NlIHdpdGggbW9zdCBhc3NvY2lhdGVkIHN0b3JlcyksIGFuZCB0aGUgZ2VvZ3JhcGhpYyBzcHJlYWQgKHRvIGVuc3VyZSBvdXIgbWFpbiBkZXBvdHMgb2ZmZXIgYWRlcXVhdGUgY292ZXJhZ2UgYWNyb3NzIHRoZSBjb3VudHJ5KS4NCg0KVGhpcyBwYXJ0IGlzIGEgc2xpZ2h0bHkgbWFudWFsIHByb2Nlc3MuDQoNCmBgYHtyfQ0KY2x1c3Rlcl9jb3VudCA8LSBzdG9yZV9sb2NhdGlvbnNfdXBkYXRlZCAlPiUNCiAgZ3JvdXBfYnkodXBkYXRlZF9jbHVzdGVyKSAlPiUNCiAgdGFsbHkoKQ0KDQpnZ3Bsb3QoY2x1c3Rlcl9jb3VudCwgYWVzKHggPSByZW9yZGVyKHVwZGF0ZWRfY2x1c3RlciwgLW4pLCB5ID0gbikpICsNCiAgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsgdGhlbWVfbWluaW1hbCgpICsNCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSAwLCBoanVzdCA9IDAuNSwgdmp1c3QgPSAwLjMpKQ0KDQpgYGANCg0KYGBge3J9DQp0b3BfNyA8LSBjKDEyLCAzLCAxMSwgMTUsIDEsIDYsIDUsIDIsIDE2KQ0KDQp0b3BfN19jbHVzdGVycyA8LSBzdG9yZV9sb2NhdGlvbnNfdXBkYXRlZCAlPiUNCiAgZmlsdGVyKHVwZGF0ZWRfY2x1c3RlciAlaW4lIHRvcF83KQ0KDQpwYWwgPC0gY29sb3JGYWN0b3IoDQogIHBhbGV0dGUgPSAiUmRZbEJ1IiwNCiAgZG9tYWluID0gdG9wXzdfY2x1c3RlcnMkdXBkYXRlZF9jbHVzdGVyKQ0KDQpwIDwtIGxlYWZsZXQodG9wXzdfY2x1c3RlcnMpICU+JSBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLlBvc2l0cm9uKSAgJT4lDQogIGFkZENpcmNsZXMofmxuZywgfmxhdCxjb2xvciA9IH5wYWwodXBkYXRlZF9jbHVzdGVyKSkgJT4lDQogIG92ZXJsYXlUaXRsZSgiU3RvcmUgQ2x1c3RlcnMiKSAlPiUNCiAgYWRkTGVnZW5kKHBvc2l0aW9uID0gImJvdHRvbXJpZ2h0IiwgdmFsdWVzID0gfnVwZGF0ZWRfY2x1c3RlciwgcGFsID0gcGFsKSAlPiUNCiAgYWRkTWFya2VycyhkYXRhID0gd2FyZWhvdXNlX2xvY2F0aW9ucywgfmxuZywgfmxhdCwgbGFiZWwgPSB+YXMuY2hhcmFjdGVyKGNsdXN0ZXIpKTsgcA0KYGBgDQoNCi0gTG9uZG9uOiBVbnN1cnByaXNpbmdseSBMb25kb24sIHdpdGggc3VjaCBhIGhpZ2ggcG9wdWxhdGlvbiwgY2FwdHVyZXMgdGhlIHR3byBsYXJnZXN0IGNsdXN0ZXJzICgzICYgMTIpLiBHaXZlbiB0aGUgc2l6ZSBvZiB0aGVzZSBjbHVzdGVycywgd2Ugd2lsbCB0YWtlIGJvdGggYXMgbWFzdGVyIGRlcG90cw0KLSBNYW5jaGVzdGVyOiBUaGUgdGhpcmQgYmlnZ2VzdCBjbHVzdGVyICgxMSkgaXMgbG9jYXRlZCBpbiBNYW5jaGVzdGVyLiBHaXZlbiB0aGUgYWJpbGl0eSB0byBzZXJ2aWNlIHRoZSBzZWNvbmQgbGFyZ2VzdCBudW1iZXIgb2Ygc3RvcmVzLCBhcyB3ZWxsIGFzIHRoZSBub3J0aGVyIHJlZ2lvbnMsIGl0IGlzIHJlYXNvbmFibGUgdG8gYWxzbyB0YWtlIHRoaXMgYXMgYSBtYXN0ZXIgZGVwb3QuDQotIEJpcm1pbmdoYW06IFRoZSBkZWNpc2lvbiBmb3IgdGhlIG5leHQgbWFzdGVyIGRlcG90LCBiZWNvbWVzIGEgd2VpZ2ggdXAgYmV0d2VlbiBjbHVzdGVycyAxICYgMTUuIEhvd2V2ZXIsIGdpdmVuIHRoZSBwcm94aW1pdHkgb2YgY2x1c3RlciAxNSB0byBNYW5jaGVzdGVyLCBJIGNob3NlIHRvIHNlbGVjdCBsb2NhdGlvbiAxIGFzIGEgbWFzdGVyIGRlcG90LCBnaXZlbiBpdCBjYW4gYmUgdXNlZCB0byBzZXJ2aWNlIHNlcnZpY2UgV2FsZXMgYW5kIHRoZSBNaWRsYW5kcywgd2l0aCB0aGUgRWFzdCBvZiBFbmdsYW5kIGJlaW5nIHNlcnZpY2VkIG91dCBvZiBNYW5jaGVzdGVyIG9yIExvbmRvbiBhcyBhcHByb3ByaWF0ZS4NCi0gR2xhc2dvdzogTGFzdGx5LCBpdCBpcyBhbHNvIHJlYXNvbmFibGUgdG8gYXNzdW1lIGEgbm9ydGhlcm4gZGVwb3QgaW4gU2NvdGxhbmQgdG8gc2VydmljZSB0aGUgY291bnRyeSwgTm9ydGggb2YgRW5nbGFuZCwgYW5kIE5vcnRoZXJuIElyZWxhbmQgc3ViLWRlcG90cy4gR2xhc2dvdyAoY2x1c3RlciAxNCkgaGFzIHRoZSBoaWdoZXN0IG51bWJlciBvZiBzdG9yZXMgYW5kIGlzIHdlbGwgbG9jYXRlZCB0byBzZXJ2aWNlIE5vcnRoZXJuIElyZWxhbmQgb24gdGhlIHdlc3QgY29hc3QuDQoNCg0KRnJvbSBtYW51YWwgaW52ZXN0aWdhdGlvbiwgYW5kIHNvbWUgYnVzaW5lc3MgbG9naWMgd2UgaGF2ZSBhc3NpZ25lZCBvdXIgNSBtYXN0ZXIgZGVwb3RzLiBIb3dldmVyLCBpdCBpcyB3b3J0aCBub3RpbmcgdGhhdCB0aGlzIGNvdWxkIGJlIGRvbmUgdXNpbmcgZHJpdmluZyBkaXN0YW5jZSAoZS5nLiBwaWNrIDUgbG9jYXRpb25zIHRoYXQgbWluaW1pc2UgYWdncmVnYXRlZCBkcml2ZSB0aW1lIHRvIHJlYWNoIGFsbCBzdWItZGVwb3RzKSwgb3Igc29tZSBhbHRlcm5hdGl2ZSBidXNpbmVzcyBsb2dpYy4gVGhpcyB3b3VsZCBiZSBhbiBhcmVhIG9mIHBvc3NpYmxlIGltcHJvdmVtZW50Lg0KYGBge3J9DQojIHBsb3QgdGhlIGZpbmFsIG1hcA0KbWFzdGVyX2RlcG90cyA8LSBjKDEsIDMsIDExLCAxMiwgMTQpDQoNCndhcmVob3VzZV9sb2NhdGlvbnMgPC0gd2FyZWhvdXNlX2xvY2F0aW9ucyAlPiUNCiAgbXV0YXRlKHRpZXIgPSBpZmVsc2UoY2x1c3RlciAlaW4lIG1hc3Rlcl9kZXBvdHMsICdtYXN0ZXInLCAnc3ViJykpICU+JQ0KICBtdXRhdGUoaWNvbl9jb2xvdXIgPSBpZmVsc2UodGllciA9PSAibWFzdGVyIiwgInJlZCIsICJibHVlIikpDQoNCmljb25zIDwtIGF3ZXNvbWVJY29ucyhpY29uQ29sb3IgPSAiYmxhY2siLA0KICAgICAgICAgICAgICAgICAgICAgIGxpYnJhcnkgPSAiaW9uIiwNCiAgICAgICAgICAgICAgICAgICAgICBtYXJrZXJDb2xvciA9IHdhcmVob3VzZV9sb2NhdGlvbnMkaWNvbl9jb2xvdXINCiAgICAgICAgICAgICAgICAgICAgICApDQoNCnBhbCA8LSBjb2xvckZhY3RvcigNCiAgcGFsZXR0ZSA9ICJSZFlsQnUiLA0KICBkb21haW4gPSBzdG9yZV9sb2NhdGlvbnNfdXBkYXRlZCR1cGRhdGVkX2NsdXN0ZXIpDQoNCnAgPC0gbGVhZmxldChzdG9yZV9sb2NhdGlvbnNfdXBkYXRlZCkgJT4lIGFkZFByb3ZpZGVyVGlsZXMocHJvdmlkZXJzJENhcnRvREIuUG9zaXRyb24pICAlPiUNCiAgYWRkQ2lyY2xlcyh+bG5nLCB+bGF0LGNvbG9yID0gfnBhbCh1cGRhdGVkX2NsdXN0ZXIpKSAlPiUNCiAgb3ZlcmxheVRpdGxlKCJTdG9yZSBDbHVzdGVycyIpICU+JQ0KICBhZGRMZWdlbmQocG9zaXRpb24gPSAiYm90dG9tcmlnaHQiLCB2YWx1ZXMgPSB+dXBkYXRlZF9jbHVzdGVyLCBwYWwgPSBwYWwpICU+JQ0KICBhZGRBd2Vzb21lTWFya2VycyhkYXRhID0gd2FyZWhvdXNlX2xvY2F0aW9ucywgfmxuZywgfmxhdCwgaWNvbiA9IGljb25zLCBsYWJlbCA9IH5hcy5jaGFyYWN0ZXIoY2x1c3RlcikpOyBwDQoNCg0KYGBgDQoNCg0KIyMjIFBvdGVudGlhbCBJbXByb3ZlbWVudHMNCg0KIyMjIyBWZWhpY2xlIFJvdXRpbmc6DQpUaGUgbmV4dCBzdGVwIHdvdWxkIGJlIHRvIGRldGVybWluZSB0aGUgbW9zdCBvcHRpbWFsIGRyaXZpbmcgcm91dGVzIGZvciB0cnVja3MgdG8gbGVhdmUgZWFjaCBvZiB0aGUgbWFzdGVyIGRlcG90cyB0byBzZXJ2aWNlIHRoZSBzdWIgZGVwb3RzIC0gaS5lIHdoYXQgaXMgdGhlIG1vc3QgZWZmaWNpZW50IG1ldGhvZCBvZiB0cnVja3MsIGxlYXZpbmcgZWFjaCBvZiB0aGUgdGhlIG1hc3RlciBkZXBvdHMsIHRvIHNlcnZpY2UgdGhlIHN1Yi1kZXBvdHMgaW4gb3VyIG5ldHdvcmsuIA0KDQpJZiB3ZSBhc3N1bWUsIGdpdmVuIHRoZSBzaXplIG9mIGRlbGl2ZXJpZXMsIGl0IGlzIHJlYWxpc3RpYyB0byBoYXZlIGEgc2luZ2xlIHRydWNrIHBlciBzdWItZGVwb3QgcmVzdG9jaywgdGhlIHByb2JsZW0gYmVjb21lcyBzaW1wbHkgYSBzaG9ydGVzdCBkdXJhdGlvbiBjYWxjdWxhdGlvbiBmcm9tIGVhY2ggc3ViLWRlcG90IHRvIHRoZSBtYXN0ZXIgZGVwb3RzIC0gd2hpY2ggT1NSTSBkb2NrZXIgY2FuIGhhbmRsZS4NCg0KSG93ZXZlciwgaWYgeW91IHdhbnQganVzdCBvbmUgdHJ1Y2sgcGVyIGRlcG90IHRvIGJlIHJlc3BvbnNpYmxlIGZvciByZXN0b2NraW5nIGVhY2ggb2YgdGhlIGFzc29jaWF0ZWQgc3ViLWRlcG90cywgdGhlbiB3ZSBuZWVkIHRvIGZyYW1lIHRoZSBwcm9ibGVtIGFzIGEgInRyYXZlbGxpbmcgc2FsZXNtYW4gcHJvYmxlbSIgLSBpLmUuIHdoYXQgaXMgdGhlIHNob3J0ZXN0IHJvdXRlIGZvciBhIHNpbmdsZSB0cnVjayB0byB2aXNpdCBhbGwgdGhlIG5lY2Vzc2FyeSBzdG9wcyBqdXN0IG9uY2UgYW5kIHJldHVybiBob21lIHRvIHRoZSBtYXN0ZXIgZGVwb3Q/IEZvciB0aGlzLCBJIHdvdWxkIHJlY29tbWVuZCBleHBsb3JpbmcgdGhlIFZST09NIFByb2plY3QgKFZlaGljbGUgUm91dGluZyBPcGVuLVNvdXJjZSBPcHRpbWlzYXRpb24gTWFjaGluZSk6IGh0dHBzOi8vZ2l0aHViLmNvbS9WUk9PTS1Qcm9qZWN0IA0KDQoNCiMjIyMgTWFzdGVyIERlcG90IFNlbGVjdGlvbjoNCkFzIG1lbnRpb25lZCBhYm92ZSwgYW5vdGhlciBwb3RlbnRpYWwgaW1wcm92ZW1lbnQgY291bGQgYmUgdG8gc2VsZWN0IHRoZSBsb2NhdGlvbiBvZiB0aGUgbWFzdGVyIGRlcG90cywgYmFzZWQgbm90IG9uIHRoZSBudW1iZXIgb2YgbG9jYXRpb25zIGFuZCBtYW51YWwgYnVzaW5lc3MgbG9naWMsIGJ1dCBiYXNlZCBvbiB3aGljaCA1IGxvY2F0aW9ucyBtaW5pbWlzZSB0aGUgZHJpdmUgdGltZSB0byBlYWNoIG9mIHRoZSByZW1haW5pbmcgc3ViLWRlcG90cy4gRm9yIHRoaXMgd2UgY291bGQgdXNlIE9TUk0gZG9ja2VyIGFsc28uDQoNCg0KIyMjIyBDbHVzdGVyIFBydW5pbmc6DQpXaGlsZSBhcmxpZXIgb24gd2UgZGlzcmVnYXJkZWQgY2x1c3RlcnMgdGhhdCB3ZXJlIGV4dHJlbWVseSByZW1vdGUsIHRoZXJlIGFyZSBzdGlsbCBhIGZldyBsb2NhdGlvbnMgY29udGFpbmVkIGluIG91ciBjbHVzdGVycyB0aGF0IGFyZSByYXRoZXIgcmVtb3RlLCB3aXRoIHRoZSBjbHVzdGVyIGJlaW5nIGZhaXJseSBzcHJlYWQgKGUuZy4gTm9ydGggRWFzdCBTY290bGFuZCkuIA0KDQpUbyBhZGRyZXNzIHRoaXMsIHdlIGNvdWxkIGl0ZXJhdGl2ZWx5IHJlbW92ZSBsb2NhdGlvbnMgZnJvbSBvdXIgZGF0YXNldCB0aGF0IGFyZSBhIGNlcnRhaW4gZGlzdGFuY2UgZnJvbSB0aGUgbmVhcmVzdCBjbHVzdGVyIGNlbnRyZSwgYW5kIHRoZW4gcGVyZm9ybSByZWNsdXN0ZXJpbmcuIFRoZSBpbnRlbnRpb24gd291bGQgYmUgdG8gcmVwZWF0ZWRseSAicHJ1bmUiIHRoZSBtb3N0IHJlbW90ZSBsb2NhdGlvbnMgaW4gYmV0d2VlbiByb3VuZHMgb2YgY2x1c3RlcmluZy4NCg0KVGhpcyB3b3VsZCByZXN1bHQgaW4gbW9yZSBkZW5zZWx5IGZvcm1lZCByZWdpb25zLCBidXQgYXQgdGhlIGV4cGVuc2Ugb2YgZXhjbHVkaW5nIGNlcnRhaW4gbG9jYXRpb25zIGZyb20gb3VyIGxvZ2lzdGljcyBuZXR3b3JrLg0KDQoNCg0KIyMjIENsb3NpbmcgQ29tbWVudHMNClRoZXJlIHdlIGhhdmUgaXQsIG91ciBjbHVzdGVyZWQgc3RvcmUgdGVycml0b3JpZXMsIHdpdGggdGhlIHByb3Bvc2VkIGxvY2F0aW9uIG9mIGVhY2ggd2FyZWhvdXNlIGFuZCBhIHByb3Bvc2VkIHZpZXcgb2Ygd2hpY2ggc2hvdWxkIG1lIG1ham9yIGRlcG90cywgYW5kIHdoaWNoIHNob3VsZCBiZSBzdWItZGVwb3RzLg0KDQpXaGlsZSB0aGVyZSBhcmUgYSBmZXcgYXJlYXMgb2YgcG90ZW50aWFsIGltcHJvdmVtZW50IG5vdGVkIGFib3ZlLCB0aGlzIHByb2plY3QgaW50cm9kdWNlcyBhbmQgaW5pdGlhbCBhcHByb2FjaCB0aGF0IGNhbiBiZSB0YWtlbiB0byBvcHRpbWlzZSBsb2dpc3RpY3MgbmV0d29ya3MgdXNpbmcgZ2Vvc3BhdGlhbCBkYXRhLiBUaGUgdG9vbHMgYW5kIHRlY2huaXF1ZXMgaW50cm9kdWNlZCBjYW4gYmUgdXNlZCBpbiB0aGVpciBjdXJyZW50IGZvcm0sIG9yIHRha2UgZnVydGhlciB3aXRoIG1vciBjb21wbGV4IGFwcHJvYWNoZXMsIHRvIGhlbHAgY29tcGFuaWVzIGdhaW4gYWN0aW9uYWJsZSBpbnNpZ2h0cyBpbnRvIHRoZWlyIGxvZ2lzdGljcywgbWFya2V0aW5nLCBvciBhY3F1aXNpdGlvbiBhcHByb2FjaGVzIC0gYW5kIGdhaW4gYSBjb21wZXRpdGl2ZSBlZGdlIGluIHRoZWlyIHJlc3BlY3RpdmUgbWFya2V0Lg==